MuranoCLI 3.0.7 → 3.0.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.ignore +1 -1
  4. data/.rubocop.yml +10 -5
  5. data/.trustme.sh +272 -106
  6. data/.trustme.vim +20 -1
  7. data/Gemfile +7 -5
  8. data/LICENSE.txt +14 -15
  9. data/MuranoCLI.gemspec +7 -5
  10. data/Rakefile +5 -5
  11. data/bin/murano +4 -4
  12. data/lib/MrMurano/Account.rb +13 -8
  13. data/lib/MrMurano/Business.rb +6 -7
  14. data/lib/MrMurano/Commander-Entry.rb +5 -5
  15. data/lib/MrMurano/Config-Migrate.rb +4 -4
  16. data/lib/MrMurano/Config.rb +27 -6
  17. data/lib/MrMurano/Content.rb +5 -5
  18. data/lib/MrMurano/Exchange-Element.rb +4 -4
  19. data/lib/MrMurano/Exchange.rb +4 -4
  20. data/lib/MrMurano/Gateway.rb +22 -11
  21. data/lib/MrMurano/Keystore.rb +4 -4
  22. data/lib/MrMurano/Logs.rb +87 -0
  23. data/lib/MrMurano/Mock.rb +5 -4
  24. data/lib/MrMurano/Passwords.rb +4 -5
  25. data/lib/MrMurano/ProjectFile.rb +4 -4
  26. data/lib/MrMurano/ReCommander.rb +4 -4
  27. data/lib/MrMurano/Setting.rb +5 -5
  28. data/lib/MrMurano/Settings-HttpService.rb +9 -6
  29. data/lib/MrMurano/Solution-ServiceConfig.rb +5 -5
  30. data/lib/MrMurano/Solution-Services.rb +76 -50
  31. data/lib/MrMurano/Solution-Users.rb +5 -4
  32. data/lib/MrMurano/Solution.rb +6 -6
  33. data/lib/MrMurano/SolutionId.rb +4 -4
  34. data/lib/MrMurano/SubCmdGroupContext.rb +4 -4
  35. data/lib/MrMurano/SyncAllowed.rb +4 -4
  36. data/lib/MrMurano/SyncRoot.rb +5 -5
  37. data/lib/MrMurano/SyncUpDown-Core.rb +515 -0
  38. data/lib/MrMurano/SyncUpDown-Item.rb +159 -0
  39. data/lib/MrMurano/SyncUpDown.rb +120 -688
  40. data/lib/MrMurano/Webservice-Cors.rb +4 -4
  41. data/lib/MrMurano/Webservice-Endpoint.rb +9 -6
  42. data/lib/MrMurano/Webservice-File.rb +5 -4
  43. data/lib/MrMurano/Webservice.rb +5 -5
  44. data/lib/MrMurano/commands/business.rb +4 -4
  45. data/lib/MrMurano/commands/completion.rb +6 -6
  46. data/lib/MrMurano/commands/config.rb +7 -5
  47. data/lib/MrMurano/commands/content.rb +5 -4
  48. data/lib/MrMurano/commands/cors.rb +4 -4
  49. data/lib/MrMurano/commands/devices.rb +6 -6
  50. data/lib/MrMurano/commands/domain.rb +4 -4
  51. data/lib/MrMurano/commands/exchange.rb +4 -4
  52. data/lib/MrMurano/commands/gb.rb +4 -4
  53. data/lib/MrMurano/commands/globals.rb +12 -4
  54. data/lib/MrMurano/commands/init.rb +5 -4
  55. data/lib/MrMurano/commands/keystore.rb +4 -4
  56. data/lib/MrMurano/commands/link.rb +4 -4
  57. data/lib/MrMurano/commands/login.rb +4 -4
  58. data/lib/MrMurano/commands/logs.rb +229 -76
  59. data/lib/MrMurano/commands/mock.rb +4 -4
  60. data/lib/MrMurano/commands/password.rb +4 -4
  61. data/lib/MrMurano/commands/postgresql.rb +4 -4
  62. data/lib/MrMurano/commands/settings.rb +4 -4
  63. data/lib/MrMurano/commands/show.rb +4 -4
  64. data/lib/MrMurano/commands/solution.rb +4 -4
  65. data/lib/MrMurano/commands/solution_picker.rb +4 -4
  66. data/lib/MrMurano/commands/status.rb +12 -4
  67. data/lib/MrMurano/commands/sync.rb +4 -4
  68. data/lib/MrMurano/commands/timeseries.rb +4 -4
  69. data/lib/MrMurano/commands/tsdb.rb +6 -7
  70. data/lib/MrMurano/commands/usage.rb +4 -4
  71. data/lib/MrMurano/commands.rb +4 -4
  72. data/lib/MrMurano/hash.rb +5 -5
  73. data/lib/MrMurano/http.rb +26 -22
  74. data/lib/MrMurano/makePretty.rb +194 -10
  75. data/lib/MrMurano/optparse.rb +1 -1
  76. data/lib/MrMurano/orderedhash.rb +1 -1
  77. data/lib/MrMurano/progress.rb +4 -4
  78. data/lib/MrMurano/verbosing.rb +6 -6
  79. data/lib/MrMurano/version.rb +5 -5
  80. data/lib/MrMurano.rb +7 -4
  81. data/spec/Account-Passwords_spec.rb +4 -4
  82. data/spec/Account_spec.rb +4 -4
  83. data/spec/Business_spec.rb +4 -4
  84. data/spec/ConfigFile_spec.rb +4 -4
  85. data/spec/ConfigMigrate_spec.rb +5 -4
  86. data/spec/Config_spec.rb +5 -4
  87. data/spec/Content_spec.rb +5 -4
  88. data/spec/GatewayBase_spec.rb +4 -4
  89. data/spec/GatewayDevice_spec.rb +4 -4
  90. data/spec/GatewayResource_spec.rb +5 -4
  91. data/spec/GatewaySettings_spec.rb +4 -4
  92. data/spec/Http_spec.rb +4 -4
  93. data/spec/MakePretties_spec.rb +20 -20
  94. data/spec/Mock_spec.rb +4 -4
  95. data/spec/ProjectFile_spec.rb +4 -4
  96. data/spec/Setting_spec.rb +4 -4
  97. data/spec/Solution-ServiceConfig_spec.rb +4 -4
  98. data/spec/Solution-ServiceEventHandler_spec.rb +5 -4
  99. data/spec/Solution-ServiceModules_spec.rb +5 -4
  100. data/spec/Solution-UsersRoles_spec.rb +5 -4
  101. data/spec/Solution_spec.rb +4 -4
  102. data/spec/SyncRoot_spec.rb +4 -4
  103. data/spec/SyncUpDown_spec.rb +67 -21
  104. data/spec/Verbosing_spec.rb +12 -10
  105. data/spec/Webservice-Cors_spec.rb +4 -4
  106. data/spec/Webservice-Endpoint_spec.rb +5 -4
  107. data/spec/Webservice-File_spec.rb +5 -4
  108. data/spec/Webservice-Setting_spec.rb +4 -4
  109. data/spec/_workspace.rb +4 -4
  110. data/spec/cmd_business_spec.rb +4 -5
  111. data/spec/cmd_common.rb +51 -20
  112. data/spec/cmd_config_spec.rb +4 -5
  113. data/spec/cmd_content_spec.rb +4 -5
  114. data/spec/cmd_cors_spec.rb +4 -5
  115. data/spec/cmd_device_spec.rb +5 -6
  116. data/spec/cmd_domain_spec.rb +4 -5
  117. data/spec/cmd_exchange_spec.rb +4 -5
  118. data/spec/cmd_help_spec.rb +4 -5
  119. data/spec/cmd_init_spec.rb +16 -35
  120. data/spec/cmd_keystore_spec.rb +4 -5
  121. data/spec/cmd_link_spec.rb +11 -12
  122. data/spec/cmd_logs_spec.rb +162 -0
  123. data/spec/cmd_password_spec.rb +4 -5
  124. data/spec/cmd_setting_application_spec.rb +4 -5
  125. data/spec/cmd_setting_product_spec.rb +4 -5
  126. data/spec/cmd_status_spec.rb +44 -81
  127. data/spec/cmd_syncdown_application_spec.rb +7 -10
  128. data/spec/cmd_syncdown_both_spec.rb +10 -25
  129. data/spec/cmd_syncup_spec.rb +31 -37
  130. data/spec/cmd_usage_spec.rb +4 -5
  131. data/spec/fixtures/dumped_config +1 -0
  132. data/spec/fixtures/websocket/logs_blather.rb +27 -0
  133. data/spec/fixtures/websocket/logs_faker.rb +153 -0
  134. data/spec/fixtures/websocket/simple_connection.rb +45 -0
  135. data/spec/fixtures/websocket/simple_options.rb +77 -0
  136. data/spec/fixtures/websocket/simple_server.rb +69 -0
  137. data/spec/fixtures/websocket/wss-echo.rb +48 -0
  138. data/spec/fixtures/websocket/wss-fake-logs.rb +20 -0
  139. metadata +55 -2
@@ -0,0 +1,159 @@
1
+ # Copyright © 2016-2017 Exosite LLC. All Rights Reserved
2
+ # License: PROPRIETARY. See LICENSE.txt.
3
+ # frozen_string_literal: true
4
+
5
+ # vim:tw=0:ts=2:sw=2:et:ai
6
+ # Unauthorized copying of this file is strictly prohibited.
7
+
8
+ require 'MrMurano/verbosing'
9
+
10
+ module MrMurano
11
+ ## The functionality of a Syncable thing.
12
+ #
13
+ # This provides the logic for computing what things have changed, and pushing and
14
+ # pulling those things.
15
+ #
16
+ module SyncUpDown
17
+ # This is one item that can be synced.
18
+ class Item
19
+ # @return [String] The name of this item.
20
+ attr_accessor :name
21
+ # @return [Pathname] Where this item lives.
22
+ attr_accessor :local_path
23
+ # FIXME/EXPLAIN: ??? what is this?
24
+ attr_accessor :id
25
+ # @return [String] The Lua code for this item. (not all items use this.)
26
+ attr_accessor :script
27
+ # @return [Integer] The line in #local_path where this #script starts.
28
+ attr_accessor :line
29
+ # @return [Integer] The line in #local_path where this #script ends.
30
+ attr_accessor :line_end
31
+ # @return [String] If requested, the diff output.
32
+ attr_accessor :diff
33
+ # @return [Boolean] When filtering, did this item pass.
34
+ attr_accessor :selected
35
+ # @return [String] The constructed name used to match local items to remote items.
36
+ attr_accessor :synckey
37
+ # @return [String] The syncable type.
38
+ attr_accessor :synctype
39
+ # @return [String] The updated_at time from the server is used to detect changes.
40
+ attr_accessor :updated_at
41
+ # @return [Integer] Positive if multiple conflicting files found for same item.
42
+ attr_accessor :dup_count
43
+
44
+ # Initialize a new Item with a few, or all, attributes.
45
+ # @param hsh [Hash{Symbol=>Object}, Item] Initial values
46
+ #
47
+ # @example Initializing with a Hash
48
+ # Item.new(:name=>'Bob', :local_path => Pathname.new(…))
49
+ # @example Initializing with an Item
50
+ # item = Item.new(:name => 'get')
51
+ # Item.new(item)
52
+ def initialize(hsh={})
53
+ hsh.each_pair { |k, v| self[k] = v }
54
+ end
55
+
56
+ def as_inst(key)
57
+ return key if key.to_s[0] == '@'
58
+ "@#{key}"
59
+ end
60
+ private :as_inst
61
+
62
+ def as_sym(key)
63
+ return key.to_sym if key.to_s[0] != '@'
64
+ key.to_s[1..-1].to_sym
65
+ end
66
+ private :as_sym
67
+
68
+ # Get attribute as if this was a Hash
69
+ # @param key [String,Symbol] attribute name
70
+ # @return [Object] The value
71
+ def [](key)
72
+ public_send(key.to_sym)
73
+ rescue NoMethodError
74
+ nil
75
+ end
76
+
77
+ # Set attribute as if this was a Hash
78
+ # @param key [String,Symbol] attribute name
79
+ # @param value [Object] value to set
80
+ def []=(key, value)
81
+ public_send("#{key}=", value)
82
+ rescue StandardError => err
83
+ MrMurano::Verbose.error(
84
+ "Unable to set key: #{key} / value: #{value} / err: #{err} / self: #{inspect}"
85
+ )
86
+ end
87
+
88
+ # Delete a key
89
+ # @param key [String,Symbol] attribute name
90
+ # @return [Object] The value
91
+ def delete(key)
92
+ inst = as_inst(key)
93
+ remove_instance_variable(inst) if instance_variable_defined?(inst)
94
+ end
95
+
96
+ # @return [Hash{Symbol=>Object}] A hash that represents this Item
97
+ def to_h
98
+ Hash[instance_variables.map { |k| [as_sym(k), instance_variable_get(k)] }]
99
+ end
100
+
101
+ # Adds the contents of item to self.
102
+ # @param item [Item,Hash] Stuff to merge
103
+ # @return [Item] ourself
104
+ def merge!(item)
105
+ item.each_pair { |k, v| self[k] = v }
106
+ self
107
+ end
108
+
109
+ # A new Item containing our plus items.
110
+ # @param item [Item,Hash] Stuff to merge
111
+ # @return [Item] New item with contents of both
112
+ def merge(item)
113
+ dup.merge!(item)
114
+ end
115
+
116
+ # Calls block once for each non-nil key
117
+ # @yieldparam key [Symbol] The name of the key
118
+ # @yieldparam value [Object] The value for that key
119
+ # @return [Item]
120
+ def each_pair
121
+ instance_variables.each do |key|
122
+ yield as_sym(key), instance_variable_get(key)
123
+ end
124
+ self
125
+ end
126
+
127
+ # Delete items in self that block returns true.
128
+ # @yieldparam key [Symbol] The name of the key
129
+ # @yieldparam value [Object] The value for that key
130
+ # @yieldreturn [Boolean] True to delete this key
131
+ # @return [Item] Ourself.
132
+ def reject!(&_block)
133
+ instance_variables.each do |key|
134
+ drop = yield as_sym(key), instance_variable_get(key)
135
+ delete(key) if drop
136
+ end
137
+ self
138
+ end
139
+
140
+ # A new Item with keys deleted where block is true
141
+ # @yieldparam key [Symbol] The name of the key
142
+ # @yieldparam value [Object] The value for that key
143
+ # @yieldreturn [Boolean] True to delete this key
144
+ # @return [Item] New Item with keys deleted
145
+ def reject(&block)
146
+ dup.reject!(&block)
147
+ end
148
+
149
+ # For unit testing.
150
+ include Comparable
151
+ def <=>(other)
152
+ # rubocop:disable Style/RedundantSelf: Redundant self detected.
153
+ # MAYBE/2017-07-18: Permanently disable Style/RedundantSelf?
154
+ self.to_h <=> other.to_h
155
+ end
156
+ end
157
+ end
158
+ end
159
+
@@ -1,173 +1,27 @@
1
- # Last Modified: 2017.11.03 /coding: utf-8
1
+ # Copyright © 2016-2017 Exosite LLC. All Rights Reserved
2
+ # License: PROPRIETARY. See LICENSE.txt.
2
3
  # frozen_string_literal: true
3
4
 
4
- # Copyright © 2016-2017 Exosite LLC.
5
- # License: MIT. See LICENSE.txt.
6
- # vim:tw=0:ts=2:sw=2:et:ai
7
-
8
- # FIXME/MAYBE: Fix semicolon usage.
9
- # rubocop:disable Style/Semicolon
5
+ # vim:tw=0:ts=2:sw=2:et:ai
6
+ # Unauthorized copying of this file is strictly prohibited.
10
7
 
11
8
  require 'inflecto'
12
- require 'open3'
13
9
  require 'os'
14
10
  require 'pathname'
15
- #require 'shellwords'
16
- require 'tempfile'
17
- require 'MrMurano/progress'
11
+ require 'time'
18
12
  require 'MrMurano/verbosing'
19
- require 'MrMurano/hash'
20
- #require 'MrMurano/Config'
21
- #require 'MrMurano/ProjectFile'
22
13
  require 'MrMurano/SyncAllowed'
23
- ##require 'MrMurano/SyncRoot'
14
+ require 'MrMurano/SyncUpDown-Core'
15
+ require 'MrMurano/SyncUpDown-Item'
24
16
 
25
17
  module MrMurano
26
18
  ## The functionality of a Syncable thing.
27
19
  #
28
- # This provides the logic for computing what things have changed, and pushing and
29
- # pulling those things.
30
- #
20
+ # This provides the logic for computing what things have changed,
21
+ # and pushing and pulling those things.
31
22
  module SyncUpDown
32
23
  include SyncAllowed
33
-
34
- # This is one item that can be synced.
35
- class Item
36
- # @return [String] The name of this item.
37
- attr_accessor :name
38
- # @return [Pathname] Where this item lives.
39
- attr_accessor :local_path
40
- # FIXME/EXPLAIN: ??? what is this?
41
- attr_accessor :id
42
- # @return [String] The Lua code for this item. (not all items use this.)
43
- attr_accessor :script
44
- # @return [Integer] The line in #local_path where this #script starts.
45
- attr_accessor :line
46
- # @return [Integer] The line in #local_path where this #script ends.
47
- attr_accessor :line_end
48
- # @return [String] If requested, the diff output.
49
- attr_accessor :diff
50
- # @return [Boolean] When filtering, did this item pass.
51
- attr_accessor :selected
52
- # @return [String] The constructed name used to match local items to remote items.
53
- attr_accessor :synckey
54
- # @return [String] The syncable type.
55
- attr_accessor :synctype
56
- # @return [String] The updated_at time from the server is used to detect changes.
57
- attr_accessor :updated_at
58
- # @return [Integer] Positive if multiple conflicting files found for same item.
59
- attr_accessor :dup_count
60
-
61
- # Initialize a new Item with a few, or all, attributes.
62
- # @param hsh [Hash{Symbol=>Object}, Item] Initial values
63
- #
64
- # @example Initializing with a Hash
65
- # Item.new(:name=>'Bob', :local_path => Pathname.new(…))
66
- # @example Initializing with an Item
67
- # item = Item.new(:name => 'get')
68
- # Item.new(item)
69
- def initialize(hsh={})
70
- hsh.each_pair { |k, v| self[k] = v }
71
- end
72
-
73
- def as_inst(key)
74
- return key if key.to_s[0] == '@'
75
- "@#{key}"
76
- end
77
- private :as_inst
78
- def as_sym(key)
79
- return key.to_sym if key.to_s[0] != '@'
80
- key.to_s[1..-1].to_sym
81
- end
82
- private :as_sym
83
-
84
- # Get attribute as if this was a Hash
85
- # @param key [String,Symbol] attribute name
86
- # @return [Object] The value
87
- def [](key)
88
- public_send(key.to_sym)
89
- end
90
-
91
- # Set attribute as if this was a Hash
92
- # @param key [String,Symbol] attribute name
93
- # @param value [Object] value to set
94
- def []=(key, value)
95
- public_send("#{key}=", value)
96
- rescue StandardError => err
97
- MrMurano::Verbose.error(
98
- "Unable to set key: #{key} / value: #{value} / err: #{err} / self: #{inspect}"
99
- )
100
- end
101
-
102
- # Delete a key
103
- # @param key [String,Symbol] attribute name
104
- # @return [Object] The value
105
- def delete(key)
106
- inst = as_inst(key)
107
- remove_instance_variable(inst) if instance_variable_defined?(inst)
108
- end
109
-
110
- # @return [Hash{Symbol=>Object}] A hash that represents this Item
111
- def to_h
112
- Hash[instance_variables.map { |k| [as_sym(k), instance_variable_get(k)] }]
113
- end
114
-
115
- # Adds the contents of item to self.
116
- # @param item [Item,Hash] Stuff to merge
117
- # @return [Item] ourself
118
- def merge!(item)
119
- item.each_pair { |k, v| self[k] = v }
120
- self
121
- end
122
-
123
- # A new Item containing our plus items.
124
- # @param item [Item,Hash] Stuff to merge
125
- # @return [Item] New item with contents of both
126
- def merge(item)
127
- dup.merge!(item)
128
- end
129
-
130
- # Calls block once for each non-nil key
131
- # @yieldparam key [Symbol] The name of the key
132
- # @yieldparam value [Object] The value for that key
133
- # @return [Item]
134
- def each_pair
135
- instance_variables.each do |key|
136
- yield as_sym(key), instance_variable_get(key)
137
- end
138
- self
139
- end
140
-
141
- # Delete items in self that block returns true.
142
- # @yieldparam key [Symbol] The name of the key
143
- # @yieldparam value [Object] The value for that key
144
- # @yieldreturn [Boolean] True to delete this key
145
- # @return [Item] Ourself.
146
- def reject!(&_block)
147
- instance_variables.each do |key|
148
- drop = yield as_sym(key), instance_variable_get(key)
149
- delete(key) if drop
150
- end
151
- self
152
- end
153
-
154
- # A new Item with keys deleted where block is true
155
- # @yieldparam key [Symbol] The name of the key
156
- # @yieldparam value [Object] The value for that key
157
- # @yieldreturn [Boolean] True to delete this key
158
- # @return [Item] New Item with keys deleted
159
- def reject(&block)
160
- dup.reject!(&block)
161
- end
162
-
163
- # For unit testing.
164
- include Comparable
165
- def <=>(other)
166
- # rubocop:disable Style/RedundantSelf: Redundant self detected.
167
- # MAYBE/2017-07-18: Permanently disable Style/RedundantSelf?
168
- self.to_h <=> other.to_h
169
- end
170
- end # MrMurano::SyncUpDown::Item
24
+ include SyncCore
171
25
 
172
26
  #######################################################################
173
27
  # Methods that must be overridden
@@ -193,6 +47,10 @@ module MrMurano
193
47
  # :nocov:
194
48
  end
195
49
 
50
+ def remove_lite(itemkey, _thereitem, _modify=false)
51
+ remove(itemkey)
52
+ end
53
+
196
54
  ## Upload local item to remote
197
55
  #
198
56
  # Children objects Must override this
@@ -300,31 +158,32 @@ module MrMurano
300
158
  #
301
159
  # @param local [Pathname] Full path of where to download to
302
160
  # @param item [Item] The item to download
303
- def download(local, item, options={})
161
+ def download(local, item, options: {}, is_tmp: false)
304
162
  #if item[:bundled]
305
163
  # warning "Not downloading into bundled item #{synckey(item)}"
306
164
  # return
307
165
  #end
308
166
  id = item[@itemkey.to_sym]
309
167
  if id.to_s.empty?
310
- # 2017-09-05: MRMUR-156: User seeing this.
311
168
  if @itemkey.to_sym != :id
312
- debug "!!! Missing '#{@itemkey}', trying :id instead"
169
+ debug "Missing '#{@itemkey}', trying :id instead"
313
170
  id = item[:id]
314
171
  end
315
172
  if id.to_s.empty?
316
- debug %(Remote item "#{item[:name]}" missing :id / local: #{local} / item: #{item})
173
+ debug %(Missing id: remote: #{item[:name]} / local: #{local} / item: #{item})
317
174
  return if options[:ignore_errors]
318
175
  error %(Remote item missing :id => #{local})
319
- print %(You can ignore this error using --ignore-errors)
176
+ say %(You can ignore this error using --ignore-errors)
320
177
  exit 1
321
178
  end
322
179
  debug ":id => #{id}"
323
180
  end
324
-
325
- relpath = local.relative_path_from(Pathname.pwd).to_s
326
- return unless download_item_allowed(relpath)
327
-
181
+ unless is_tmp
182
+ relpath = local.relative_path_from(Pathname.pwd).to_s
183
+ return unless download_item_allowed(relpath)
184
+ end
185
+ # MAYBE: If is_tmp and doing syncdown, just use this file rather
186
+ # than downloading again.
328
187
  local.dirname.mkpath
329
188
  local.open('wb') do |io|
330
189
  fetch(id) do |chunk|
@@ -334,8 +193,8 @@ module MrMurano
334
193
  update_mtime(local, item)
335
194
  end
336
195
 
337
- def diff_download(tmp_path, merged)
338
- download(tmp_path, merged)
196
+ def diff_download(tmp_path, merged, options)
197
+ download(tmp_path, merged, options: options, is_tmp: true)
339
198
  end
340
199
 
341
200
  ## Give the local file the same timestamp as the remote, because diff.
@@ -350,7 +209,7 @@ module MrMurano
350
209
  return unless item[:updated_at]
351
210
 
352
211
  mod_time = item[:updated_at]
353
- mod_time = DateTime.parse(mod_time).to_time unless mod_time.is_a?(Time)
212
+ mod_time = Time.parse(mod_time) unless mod_time.is_a?(Time)
354
213
  begin
355
214
  FileUtils.touch([local.to_path], mtime: mod_time)
356
215
  rescue Errno::EACCES => err
@@ -362,7 +221,7 @@ module MrMurano
362
221
  #)
363
222
  unless OS.windows?
364
223
  msg = 'Unexpected: touch failed on non-Windows machine'
365
- $stderr.puts("#{msg} / host_os: #{RbConfig::CONFIG['host_os']} / err: #{err}")
224
+ warn "#{msg} / host_os: #{RbConfig::CONFIG['host_os']} / err: #{err}"
366
225
  end
367
226
 
368
227
  # 2017-07-13: Nor does ctime work.
@@ -420,6 +279,7 @@ module MrMurano
420
279
  end
421
280
 
422
281
  def syncup_after
282
+ 0
423
283
  end
424
284
 
425
285
  def syncdown_before
@@ -427,6 +287,7 @@ module MrMurano
427
287
  end
428
288
 
429
289
  def syncdown_after(_local)
290
+ 0
430
291
  end
431
292
 
432
293
  def diff_item_write(io, merged, _local, _remote)
@@ -453,89 +314,107 @@ module MrMurano
453
314
  # This collects items in the project and all bundles.
454
315
  # @return [Array<Item>] items found
455
316
  #
456
- # 2017-07-02: [lb] removed this commented-out code from the locallist
457
- # body. I think it's for older Solutionfiles, like 0.2.0 and 0.3.0.
458
- #def locallist
459
- # # so. if @locationbase/bundles exists
460
- # # gather and merge: @locationbase/bundles/*/@location
461
- # # then merge @locationbase/@location
462
- # #
463
- # bundleDir = $cfg['location.bundles'] or 'bundles'
464
- # bundleDir = 'bundles' if bundleDir.nil?
465
- # items = {}
466
- # if (@locationbase + bundleDir).directory?
467
- # (@locationbase + bundleDir).children.sort.each do |bndl|
468
- # if (bndl + @location).exist?
469
- # verbose("Loading from bundle #{bndl.basename}")
470
- # bitems = localitems(bndl + @location)
471
- # bitems.map!{|b| b[:bundled] = true; b} # mark items from bundles.
472
- # # use synckey for quicker merging.
473
- # bitems.each { |b| items[synckey(b)] = b }
317
+ # 2017-07-02: [lb] removed this commented-out code from locallist body.
318
+ # See "Bundles" comments in TODO.taskpaper.
319
+ # This code builds the list of local items from all bundle
320
+ # subdirectories. Would that be how a bundles implementation
321
+ # works? Or would we rather just iterate over each bundle and
322
+ # process them separately, rather than all together at once?
323
+ #
324
+ # def locallist
325
+ # # so. if @locationbase/bundles exists
326
+ # # gather and merge: @locationbase/bundles/*/@location
327
+ # # then merge @locationbase/@location
328
+ # #
329
+ # bundleDir = $cfg['location.bundles'] or 'bundles'
330
+ # bundleDir = 'bundles' if bundleDir.nil?
331
+ # items = {}
332
+ # if (@locationbase + bundleDir).directory?
333
+ # (@locationbase + bundleDir).children.sort.each do |bndl|
334
+ # if (bndl + @location).exist?
335
+ # verbose("Loading from bundle #{bndl.basename}")
336
+ # bitems = localitems(bndl + @location)
337
+ # bitems.map!{|b| b[:bundled] = true; b} # mark items from bundles.
338
+ # # use synckey for quicker merging.
339
+ # bitems.each { |b| items[synckey(b)] = b }
340
+ # end
341
+ # end
474
342
  # end
475
343
  # end
476
- # end
477
- #end
478
344
  #
479
345
  def locallist(skip_warn: false)
480
346
  items = {}
481
347
  if location.exist?
482
348
  # Get a list of SyncUpDown::Item's, or a class derived thereof.
483
349
  bitems = localitems(location)
484
- # Use synckey for quicker merging.
485
- #bitems.each { |b| items[synckey(b)] = b }
486
- # 2017-07-02: If two files have the same identity, the simple loop
487
- # masks that there are two files with the same identity. So check
488
- # first for duplicates, and then process each item.
489
- seen = {}
490
- bitems.each do |item|
491
- skey = synckey(item)
492
- seen[skey] = seen.key?(skey) && seen[skey] + 1 || 1
493
- end
350
+ # Check for duplicates first -- two files with the same identity.
351
+ seen = locallist_mark_seen(bitems)
494
352
  counts = {}
495
353
  bitems.each do |item|
496
- skey = synckey(item)
497
- if seen[skey] > 1
498
- if items[skey].nil?
499
- items[skey] = MrMurano::EventHandler::EventHandlerItem.new(item)
500
- items[skey][:dup_count] = 0
501
- end
502
- counts[skey] = counts.key?(skey) && counts[skey] + 1 || 1
503
- # Use a unique synckey so all duplicates make it in the list.
504
- uniq_synckey = "#{skey}-#{counts[skey]}"
505
- item[:dup_count] = counts[skey]
506
- # This sets the alias for the output, so duplicates look unique.
507
- item[@itemkey.to_sym] = uniq_synckey
508
- items[uniq_synckey] = item
509
- msg = "Duplicate definition found for #{fancy_ticks(skey)}"
510
- if self.class.description.to_s != ''
511
- msg += " for #{fancy_ticks(self.class.description)}"
512
- end
513
- warning(msg)
514
- warning(" #{item.local_path}")
515
- else
516
- items[skey] = item
517
- end
354
+ locallist_add_item(item, items, seen, counts)
518
355
  end
519
356
  elsif !skip_warn
520
- @missing_complaints = [] unless defined?(@missing_complaints)
521
- unless @missing_complaints.include?(location)
522
- # MEH/2017-07-31: This message is a little misleading on syncdown,
523
- # e.g., in rspec ./spec/cmd_syncdown_spec.rb, one test blows away
524
- # local directories and does a syncdown, and on stderr you'll see
525
- # Skipping missing location
526
- # ‘/tmp/d20170731-3150-1f50uj4/project/specs/resources.yaml’ (Resources)
527
- # but then later in the syncdown, that directory and file gets created.
528
- msg = "Skipping missing location #{fancy_ticks(location)}"
529
- unless self.class.description.to_s.empty?
530
- msg += " (#{Inflecto.pluralize(self.class.description)})"
531
- end
532
- warning(msg)
533
- @missing_complaints << location
534
- end
357
+ locallist_complain_missing
535
358
  end
536
359
  items.values
537
360
  end
538
361
 
362
+ def locallist_mark_seen(bitems)
363
+ seen = {}
364
+ bitems.each do |item|
365
+ skey = synckey(item)
366
+ seen[skey] = seen.key?(skey) && seen[skey] + 1 || 1
367
+ end
368
+ seen
369
+ end
370
+
371
+ def locallist_add_item(item, items, seen, counts)
372
+ skey = synckey(item)
373
+ if seen[skey] > 1
374
+ if items[skey].nil?
375
+ items[skey] = item.clone
376
+ items[skey][:dup_count] = 0
377
+ end
378
+ counts[skey] = counts.key?(skey) && counts[skey] + 1 || 1
379
+ # Use a unique synckey so all duplicates make it in the list.
380
+ uniq_synckey = "#{skey}-#{counts[skey]}"
381
+ item[:dup_count] = counts[skey]
382
+ # This sets the alias for the output, so duplicates look unique.
383
+ item[@itemkey.to_sym] = uniq_synckey
384
+ items[uniq_synckey] = item
385
+ msg = "Duplicate definition found for #{fancy_ticks(skey)}"
386
+ if self.class.description.to_s != ''
387
+ msg += " for #{fancy_ticks(self.class.description)}"
388
+ end
389
+ warning(msg)
390
+ warning(" #{item.local_path}")
391
+ else
392
+ items[skey] = item
393
+ end
394
+ end
395
+
396
+ def locallist_complain_missing
397
+ @missing_complaints = [] unless defined?(@missing_complaints)
398
+ return if @missing_complaints.include?(location)
399
+ # MEH/2017-07-31: This message is a little misleading on syncdown,
400
+ # e.g., in rspec ./spec/cmd_syncdown_spec.rb, one test blows away
401
+ # local directories and does a syncdown, and on stderr you'll see
402
+ # Skipping missing location
403
+ # ‘/tmp/d20170731-3150-1f50uj4/project/specs/resources.yaml’ (Resources)
404
+ # but then later in the syncdown, that directory and file gets created.
405
+ msg = "Skipping missing location #{fancy_ticks(location)}"
406
+ unless self.class.description.to_s.empty?
407
+ msg += " (#{Inflecto.pluralize(self.class.description)})"
408
+ end
409
+ warning(msg)
410
+ @missing_complaints << location
411
+ end
412
+
413
+ # Some items are considered "undeletable", meaning if a corresponding
414
+ # file does not exist locally, or if the user deletes such a file, we
415
+ # do not delete it on the server, but instead set it to the empty string.
416
+ # The reverse is also true: if a service script on the platform is empty,
417
+ # we do not need to create a file for it locally.
539
418
  def resurrect_undeletables(localbox, _therebox)
540
419
  # It's up to the Syncables to implement this, if they care.
541
420
  localbox
@@ -589,7 +468,7 @@ module MrMurano
589
468
  if ::File.directory?(path)
590
469
  true
591
470
  else
592
- ignoring.any? { |pattern| self.ignore?(path, pattern) }
471
+ ignoring.any? { |pattern| ignore?(path, pattern) }
593
472
  end
594
473
  end
595
474
  items = items.map do |path|
@@ -604,7 +483,10 @@ module MrMurano
604
483
  end
605
484
  item = to_remote_item(from, rpath)
606
485
  if item.is_a?(Array)
607
- item.compact.map { |i| i[:local_path] = rpath; i }
486
+ item.compact.map do |itm|
487
+ itm[:local_path] = rpath
488
+ itm
489
+ end
608
490
  elsif !item.nil?
609
491
  item[:local_path] = rpath
610
492
  item
@@ -649,456 +531,6 @@ module MrMurano
649
531
  def config_vars_encode(script)
650
532
  script
651
533
  end
652
-
653
- #######################################################################
654
- # Methods that provide the core status/syncup/syncdown
655
-
656
- def sync_update_progress(msg)
657
- if $cfg['tool.no-progress']
658
- say(msg)
659
- else
660
- MrMurano::Verbose.verbose(msg + "\n")
661
- end
662
- end
663
-
664
- ## Make things in Murano look like local project
665
- #
666
- # This creates, uploads, and deletes things as needed up in Murano to match
667
- # what is in the local project directory.
668
- #
669
- # @param options [Hash, Commander::Command::Options] Options on operation
670
- # @param selected [Array<String>] Filters for _matcher
671
- def syncup(options={}, selected=[])
672
- return 0 unless api_id?
673
-
674
- options = elevate_hash(options)
675
- options[:asdown] = false
676
-
677
- num_synced = 0
678
-
679
- syncup_before
680
-
681
- dt = status(options, selected)
682
-
683
- toadd = dt[:toadd]
684
- todel = dt[:todel]
685
- tomod = dt[:tomod]
686
-
687
- itemkey = @itemkey.to_sym
688
- todel.each do |item|
689
- syncup_item(item, options, :delete, 'Removing') do |aitem|
690
- remove(aitem[itemkey])
691
- num_synced += 1
692
- end
693
- end
694
- toadd.each do |item|
695
- syncup_item(item, options, :create, 'Adding') do |aitem|
696
- upload(aitem[:local_path], aitem.reject { |k, _v| k == :local_path }, false)
697
- num_synced += 1
698
- end
699
- end
700
- tomod.each do |item|
701
- syncup_item(item, options, :update, 'Updating') do |aitem|
702
- upload(aitem[:local_path], aitem.reject { |k, _v| k == :local_path }, true)
703
- num_synced += 1
704
- end
705
- end
706
-
707
- syncup_after
708
-
709
- MrMurano::Verbose.whirly_stop(force: true)
710
-
711
- num_synced
712
- end
713
-
714
- def syncup_item(item, options, action, verbage)
715
- if options[action]
716
- # It's up to the callback to check and honor $cfg['tool.dry'].
717
- prog_msg = "#{verbage.capitalize} item #{item[:synckey]}"
718
- prog_msg += " (#{item[:synctype]})" if $cfg['tool.verbose']
719
- sync_update_progress(prog_msg)
720
- yield item
721
- elsif $cfg['tool.verbose']
722
- MrMurano::Verbose.whirly_interject do
723
- say("--no-#{action}: Not #{verbage.downcase} item #{item[:synckey]}")
724
- end
725
- end
726
- end
727
-
728
- ## Make things in local project look like Murano
729
- #
730
- # This creates, downloads, and deletes things as needed up in the local project
731
- # directory to match what is in Murano.
732
- #
733
- # @param options [Hash, Commander::Command::Options] Options on operation
734
- # @param selected [Array<String>] Filters for _matcher
735
- def syncdown(options={}, selected=[])
736
- return 0 unless api_id?
737
-
738
- options = elevate_hash(options)
739
- options[:asdown] = true
740
- options[:skip_missing_warning] = true
741
-
742
- num_synced = 0
743
-
744
- syncdown_before
745
-
746
- dt = status(options, selected)
747
-
748
- toadd = dt[:toadd]
749
- todel = dt[:todel]
750
- tomod = dt[:tomod]
751
-
752
- into = location
753
- todel.each do |item|
754
- syncdown_item(item, into, options, :delete, 'Removing') do |dest, aitem|
755
- removelocal(dest, aitem)
756
- num_synced += 1
757
- end
758
- end
759
- toadd.each do |item|
760
- syncdown_item(item, into, options, :create, 'Adding') do |dest, aitem|
761
- download(dest, aitem, options)
762
- num_synced += 1
763
- end
764
- end
765
- tomod.each do |item|
766
- syncdown_item(item, into, options, :update, 'Updating') do |dest, aitem|
767
- download(dest, aitem, options)
768
- num_synced += 1
769
- end
770
- end
771
- syncdown_after(into)
772
-
773
- num_synced
774
- end
775
-
776
- def syncdown_item(item, into, options, action, verbage)
777
- if options[action]
778
- prog_msg = "#{verbage.capitalize} item #{item[:synckey]}"
779
- prog_msg += " (#{item[:synctype]})" if $cfg['tool.verbose']
780
- sync_update_progress(prog_msg)
781
- dest = tolocalpath(into, item)
782
- yield dest, item
783
- elsif $cfg['tool.verbose']
784
- say("--no-#{action}: Not #{verbage.downcase} item #{item[:synckey]}")
785
- end
786
- end
787
-
788
- ## Call external diff tool on item
789
- #
790
- # WARNING: This will download the remote item to do the diff.
791
- #
792
- # @param merged [merged] The merged item to get a diff of
793
- # @local local, unadulterated (non-merged) item
794
- # @return [String] The diff output
795
- def dodiff(merged, local, _there=nil, asdown=false)
796
- trmt = Tempfile.new([tolocalname(merged, @itemkey) + '_remote_', '.lua'])
797
- tlcl = Tempfile.new([tolocalname(merged, @itemkey) + '_local_', '.lua'])
798
- Pathname.new(tlcl.path).open('wb') do |io|
799
- if merged.key?(:script)
800
- io << config_vars_decode(merged[:script])
801
- else
802
- # For most items, read the local file.
803
- # For resources, it's a bit trickier.
804
- # NOTE: This class adds a :selected key to the local item that we need
805
- # to remove, since it's not part of the remote items that gets downloaded.
806
- local = local.reject { |k, _v| k == :selected } unless local.nil?
807
- diff_item_write(io, merged, local, nil)
808
- end
809
- end
810
- stdout_and_stderr = ''
811
- begin
812
- tmp_path = Pathname.new(trmt.path)
813
- diff_download(tmp_path, merged)
814
-
815
- MrMurano::Verbose.whirly_stop
816
-
817
- # 2017-07-03: No worries, Ruby 3.0 frozen string literals, cmd is a list.
818
- cmd = $cfg['diff.cmd'].shellsplit
819
- # ALT_SEPARATOR is the platform specific alternative separator,
820
- # for Windows support.
821
- remote_path = trmt.path.gsub(
822
- ::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR
823
- )
824
- local_path = tlcl.path.gsub(
825
- ::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR
826
- )
827
- if asdown
828
- cmd << local_path
829
- cmd << remote_path
830
- else
831
- cmd << remote_path
832
- cmd << local_path
833
- end
834
-
835
- stdout_and_stderr, _status = Open3.capture2e(*cmd)
836
- # How important are the first two lines of the diff? E.g.,
837
- # --- /tmp/raw_data_remote_20170718-20183-gdyeg9.lua 2017-07-18 13:13:13.864051905 -0500
838
- # +++ /tmp/raw_data_local_20170718-20183-71o4me.lua 2017-07-18 13:13:14.520049397 -0500
839
- # Seems like printing the path to a since-deleted temporary file is misleading.
840
- if $cfg['diff.cmd'] == 'diff' || $cfg['diff.cmd'].start_with?('diff ')
841
- lineno = 0
842
- consise = stdout_and_stderr.lines.reject do |line|
843
- lineno += 1
844
- if lineno == 1 && line.start_with?('--- ')
845
- true
846
- elsif lineno == 2 && line.start_with?('+++ ')
847
- true
848
- else
849
- false
850
- end
851
- end
852
- stdout_and_stderr = consise.join
853
- end
854
- ensure
855
- trmt.close
856
- trmt.unlink
857
- tlcl.close
858
- tlcl.unlink
859
- end
860
- stdout_and_stderr
861
- end
862
-
863
- ##
864
- # Check if an item matches a pattern.
865
- # @param items [Array<Item>] Of items to filter
866
- # @param patterns [Array<String>] Filters for _matcher
867
- def _matcher(items, patterns)
868
- items.map do |item|
869
- if patterns.empty?
870
- item[:selected] = true
871
- else
872
- item[:selected] = patterns.any? do |pattern|
873
- if pattern.to_s[0] == '#'
874
- match(item, pattern)
875
- elsif !defined?(item.local_path) || item.local_path.nil?
876
- false
877
- else
878
- item[:local_path].fnmatch(pattern)
879
- end
880
- end
881
- end
882
- item
883
- end
884
- end
885
- private :_matcher
886
-
887
- ## Get status of things here verses there
888
- #
889
- # @param options [Hash, Commander::Command::Options] Options on operation
890
- # @param selected [Array<String>] Filters for _matcher
891
- # @return [Hash{Symbol=>Array<Item>}] Items grouped by the action that should be taken
892
- def status(options={}, selected=[])
893
- options = elevate_hash(options)
894
-
895
- ret = filter_solution(options)
896
- return ret unless ret.nil?
897
-
898
- therebox, localbox = items_lists(options, selected)
899
-
900
- toadd, todel = items_new_and_old(options, therebox, localbox)
901
-
902
- tomod, unchg = items_mods_and_chgs(options, therebox, localbox)
903
-
904
- clash = items_cull_clashes!([toadd, todel, tomod, unchg])
905
-
906
- if options[:unselected]
907
- {
908
- toadd: toadd,
909
- todel: todel,
910
- tomod: tomod,
911
- unchg: unchg,
912
- skipd: [],
913
- clash: clash,
914
- }
915
- else
916
- {
917
- toadd: select_selected(toadd),
918
- todel: select_selected(todel),
919
- tomod: select_selected(tomod),
920
- unchg: select_selected(unchg),
921
- skipd: [],
922
- clash: select_selected(clash),
923
- }
924
- end
925
- end
926
-
927
- def filter_solution(options)
928
- # Get the solution name from the config.
929
- # Convert, e.g., application.id => application.name
930
- soln_name = $cfg[@solntype.gsub(/(.*)\.id/, '\1.name')]
931
- # Skip this syncable if the api_id is not set, or if user wants to skip
932
- # by solution.
933
- skip_sol = false
934
- if !api_id? ||
935
- (options[:type] == :application && @solntype != 'application.id') ||
936
- (options[:type] == :product && @solntype != 'product.id')
937
- skip_sol = true
938
- else
939
- tested = false
940
- passed = false
941
- if @solntype == 'application.id'
942
- # elevate_hash makes the hash return false rather than
943
- # nil on unknown keys, so preface with a key? guard.
944
- if options.key?(:application) && !options[:application].to_s.empty?
945
- if soln_name =~ /#{Regexp.escape(options[:application])}/i ||
946
- api_id =~ /#{Regexp.escape(options[:application])}/i
947
- passed = true
948
- end
949
- tested = true
950
- end
951
- if options.key?(:application_id) && !options[:application_id].to_s.empty?
952
- passed = true if options[:application_id] == api_id
953
- tested = true
954
- end
955
- if options.key?(:application_name) && !options[:application_name].to_s.empty?
956
- passed = true if options[:application_name] == soln_name
957
- tested = true
958
- end
959
- elsif @solntype == 'product.id'
960
- if options.key?(:product) && !options[:product].to_s.empty?
961
- if soln_name =~ /#{Regexp.escape(options[:product])}/i ||
962
- api_id =~ /#{Regexp.escape(options[:product])}/i
963
- passed = true
964
- end
965
- tested = true
966
- end
967
- if options.key?(:product_id) && !options[:product_id].to_s.empty?
968
- passed = true if options[:product_id] == api_id
969
- tested = true
970
- end
971
- if options.key?(:product_name) && !options[:product_name].to_s.empty?
972
- passed = true if options[:product_name] == soln_name
973
- tested = true
974
- end
975
- end
976
- skip_sol = true if tested && !passed
977
- end
978
- return nil unless skip_sol
979
- ret = { toadd: [], todel: [], tomod: [], unchg: [], skipd: [], clash: [] }
980
- ret[:skipd] << { synckey: self.class.description }
981
- ret
982
- end
983
-
984
- def syncable_validate_api_id
985
- # 2017-07-02: Now that there are multiple solution types, and because
986
- # SyncRoot.add is called on different classes that go with either or
987
- # both products and applications, if a user only created one solution,
988
- # then some syncables will have their api_id set to -1, because there's
989
- # not a corresponding solution in Murano.
990
- raise 'Syncable missing api_id or not valid_api_id??!' unless api_id?
991
- end
992
-
993
- def items_lists(options, selected)
994
- # Fetch arrays of items there, and items here/local.
995
- there = list
996
- local = locallist(skip_warn: options[:skip_missing_warning])
997
-
998
- resolve_config_var_usage!(there, local)
999
-
1000
- there = _matcher(there, selected)
1001
- local = _matcher(local, selected)
1002
-
1003
- therebox = {}
1004
- there.each do |item|
1005
- item[:synckey] = synckey(item)
1006
- item[:synctype] = self.class.description
1007
- therebox[item[:synckey]] = item
1008
- end
1009
-
1010
- localbox = {}
1011
- local.each do |item|
1012
- skey = synckey(item)
1013
- # 2017-07-02: Check for local duplicates.
1014
- unless item[:dup_count].nil? || item[:dup_count].zero?
1015
- skey += "-#{item[:dup_count]}"
1016
- end
1017
- item[:synckey] = skey
1018
- item[:synctype] = self.class.description
1019
- localbox[skey] = item
1020
- end
1021
-
1022
- # Some items are considered "undeletable", meaning if a
1023
- # corresponding file does not exist locally, we assume
1024
- # it does but is just set to the empty string.
1025
- localbox = resurrect_undeletables(localbox, therebox)
1026
-
1027
- [therebox, localbox]
1028
- end
1029
-
1030
- def items_new_and_old(options, therebox, localbox)
1031
- if options[:asdown]
1032
- todel = (localbox.keys - therebox.keys).map { |key| localbox[key] }
1033
- toadd = (therebox.keys - localbox.keys).map { |key| therebox[key] }
1034
- else
1035
- toadd = (localbox.keys - therebox.keys).map { |key| localbox[key] }
1036
- todel = (therebox.keys - localbox.keys).map { |key| therebox[key] }
1037
- end
1038
- [sort_by_name(toadd), sort_by_name(todel)]
1039
- end
1040
-
1041
- def items_mods_and_chgs(options, therebox, localbox)
1042
- tomod = []
1043
- unchg = []
1044
-
1045
- (localbox.keys & therebox.keys).each do |key|
1046
- # Skip this item if it's got duplicate conflicts.
1047
- next if !localbox[key].is_a?(Hash) && localbox[key].dup_count == 0
1048
- # Want 'local' to override 'there' except for itemkey.
1049
- if options[:asdown]
1050
- mrg = therebox[key].reject { |k, _v| k == @itemkey.to_sym }
1051
- mrg = localbox[key].merge(mrg)
1052
- else
1053
- mrg = localbox[key].reject { |k, _v| k == @itemkey.to_sym }
1054
- mrg = therebox[key].merge(mrg)
1055
- end
1056
-
1057
- if docmp(localbox[key], therebox[key])
1058
- if options[:diff] && mrg[:selected]
1059
- mrg[:diff] = dodiff(mrg.to_h, localbox[key], therebox[key], options[:asdown])
1060
- end
1061
- tomod << mrg
1062
- else
1063
- unchg << mrg
1064
- end
1065
- end
1066
- [sort_by_name(tomod), sort_by_name(unchg)]
1067
- end
1068
-
1069
- def sort_by_name(list)
1070
- if list.any? && list.first.is_a?(Hash)
1071
- # AFAIK, only SyncUpDown_spec.rb comes through here, because
1072
- # it does not use SyncUpDown::Item but mocks its own items
1073
- # using hashes (see calls to and_return). [lb]
1074
- list.sort_by { |hsh| hsh[:name] }
1075
- else
1076
- list.sort_by(&:name)
1077
- end
1078
- end
1079
-
1080
- def select_selected(items)
1081
- items.select { |i| i[:selected] }.map { |i| i.delete(:selected); i }
1082
- end
1083
-
1084
- def items_cull_clashes!(items_list)
1085
- items_list = [items_list] unless items_list.is_a?(Array)
1086
- clash = []
1087
- items_list.each do |items|
1088
- items.select! do |item|
1089
- if item[:dup_count].nil?
1090
- true
1091
- elsif item[:dup_count].zero?
1092
- # This is the control item.
1093
- false
1094
- else
1095
- clash.push(item)
1096
- false
1097
- end
1098
- end
1099
- end
1100
- clash
1101
- end
1102
534
  end
1103
535
  end
1104
536