MuranoCLI 2.2.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.agignore +3 -0
  3. data/.gitignore +18 -1
  4. data/.rubocop.yml +222 -0
  5. data/.trustme.sh +185 -0
  6. data/.trustme.vim +24 -0
  7. data/Gemfile +23 -4
  8. data/LICENSE.txt +1 -1
  9. data/MuranoCLI.gemspec +43 -8
  10. data/README.markdown +9 -11
  11. data/Rakefile +187 -143
  12. data/TODO.taskpaper +2 -2
  13. data/bin/murano +51 -52
  14. data/docs/basic_example.rst +436 -0
  15. data/docs/completions/murano_completion-bash +3484 -0
  16. data/docs/demo.md +32 -32
  17. data/docs/develop.rst +391 -0
  18. data/lib/MrMurano.rb +21 -7
  19. data/lib/MrMurano/Account.rb +159 -174
  20. data/lib/MrMurano/Business.rb +381 -0
  21. data/lib/MrMurano/Config-Migrate.rb +32 -26
  22. data/lib/MrMurano/Config.rb +407 -128
  23. data/lib/MrMurano/Content.rb +191 -0
  24. data/lib/MrMurano/Gateway.rb +489 -0
  25. data/lib/MrMurano/Keystore.rb +48 -0
  26. data/lib/MrMurano/Passwords.rb +103 -0
  27. data/lib/MrMurano/ProjectFile.rb +121 -79
  28. data/lib/MrMurano/ReCommander.rb +114 -10
  29. data/lib/MrMurano/Setting.rb +90 -0
  30. data/lib/MrMurano/Solution-ServiceConfig.rb +89 -45
  31. data/lib/MrMurano/Solution-Services.rb +461 -166
  32. data/lib/MrMurano/Solution-Users.rb +70 -31
  33. data/lib/MrMurano/Solution.rb +372 -13
  34. data/lib/MrMurano/SolutionId.rb +73 -0
  35. data/lib/MrMurano/SyncRoot.rb +137 -0
  36. data/lib/MrMurano/SyncUpDown.rb +594 -284
  37. data/lib/MrMurano/Webservice-Cors.rb +71 -0
  38. data/lib/MrMurano/Webservice-Endpoint.rb +234 -0
  39. data/lib/MrMurano/Webservice-File.rb +193 -0
  40. data/lib/MrMurano/Webservice.rb +51 -0
  41. data/lib/MrMurano/commands.rb +18 -15
  42. data/lib/MrMurano/commands/business.rb +300 -6
  43. data/lib/MrMurano/commands/completion-bash.erb +166 -0
  44. data/lib/MrMurano/commands/{zshcomplete.erb → completion-zsh.erb} +0 -0
  45. data/lib/MrMurano/commands/completion.rb +76 -39
  46. data/lib/MrMurano/commands/config.rb +108 -44
  47. data/lib/MrMurano/commands/content.rb +115 -72
  48. data/lib/MrMurano/commands/cors.rb +29 -14
  49. data/lib/MrMurano/commands/devices.rb +286 -0
  50. data/lib/MrMurano/commands/domain.rb +52 -12
  51. data/lib/MrMurano/commands/gb.rb +24 -9
  52. data/lib/MrMurano/commands/globals.rb +64 -0
  53. data/lib/MrMurano/commands/init.rb +377 -155
  54. data/lib/MrMurano/commands/keystore.rb +92 -82
  55. data/lib/MrMurano/commands/link.rb +300 -0
  56. data/lib/MrMurano/commands/login.rb +74 -11
  57. data/lib/MrMurano/commands/logs.rb +63 -32
  58. data/lib/MrMurano/commands/mock.rb +57 -29
  59. data/lib/MrMurano/commands/password.rb +57 -39
  60. data/lib/MrMurano/commands/postgresql.rb +127 -94
  61. data/lib/MrMurano/commands/settings.rb +203 -0
  62. data/lib/MrMurano/commands/show.rb +79 -38
  63. data/lib/MrMurano/commands/solution.rb +423 -5
  64. data/lib/MrMurano/commands/solution_picker.rb +547 -0
  65. data/lib/MrMurano/commands/status.rb +195 -61
  66. data/lib/MrMurano/commands/sync.rb +78 -39
  67. data/lib/MrMurano/commands/timeseries.rb +71 -55
  68. data/lib/MrMurano/commands/tsdb.rb +113 -87
  69. data/lib/MrMurano/commands/usage.rb +57 -15
  70. data/lib/MrMurano/hash.rb +100 -10
  71. data/lib/MrMurano/http.rb +187 -43
  72. data/lib/MrMurano/makePretty.rb +16 -14
  73. data/lib/MrMurano/optparse.rb +2178 -0
  74. data/lib/MrMurano/progress.rb +138 -0
  75. data/lib/MrMurano/schema/resource-v1.0.0.yaml +32 -0
  76. data/lib/MrMurano/template/projectFile.murano.erb +16 -13
  77. data/lib/MrMurano/verbosing.rb +166 -29
  78. data/lib/MrMurano/version.rb +30 -1
  79. data/spec/Account-Passwords_spec.rb +21 -4
  80. data/spec/Account_spec.rb +69 -146
  81. data/spec/Business_spec.rb +290 -0
  82. data/spec/ConfigFile_spec.rb +1 -0
  83. data/spec/ConfigMigrate_spec.rb +12 -8
  84. data/spec/Config_spec.rb +40 -34
  85. data/spec/Content_spec.rb +363 -0
  86. data/spec/GatewayBase_spec.rb +54 -0
  87. data/spec/GatewayDevice_spec.rb +321 -0
  88. data/spec/GatewayResource_spec.rb +266 -0
  89. data/spec/GatewaySettings_spec.rb +120 -0
  90. data/spec/Http_spec.rb +18 -8
  91. data/spec/Mock_spec.rb +2 -2
  92. data/spec/ProjectFile_spec.rb +25 -14
  93. data/spec/Setting_spec.rb +110 -0
  94. data/spec/Solution-ServiceConfig_spec.rb +44 -5
  95. data/spec/Solution-ServiceEventHandler_spec.rb +23 -14
  96. data/spec/Solution-ServiceModules_spec.rb +47 -37
  97. data/spec/Solution-UsersRoles_spec.rb +10 -8
  98. data/spec/Solution_spec.rb +17 -8
  99. data/spec/SyncRoot_spec.rb +46 -20
  100. data/spec/SyncUpDown_spec.rb +437 -201
  101. data/spec/Verbosing_spec.rb +12 -4
  102. data/spec/{Solution-Cors_spec.rb → Webservice-Cors_spec.rb} +23 -20
  103. data/spec/{Solution-Endpoint_spec.rb → Webservice-Endpoint_spec.rb} +43 -41
  104. data/spec/{Solution-File_spec.rb → Webservice-File_spec.rb} +44 -33
  105. data/spec/Webservice-Setting_spec.rb +89 -0
  106. data/spec/_workspace.rb +4 -4
  107. data/spec/cmd_business_spec.rb +9 -4
  108. data/spec/cmd_common.rb +44 -1
  109. data/spec/cmd_content_spec.rb +43 -17
  110. data/spec/cmd_cors_spec.rb +4 -4
  111. data/spec/cmd_device_spec.rb +61 -16
  112. data/spec/cmd_domain_spec.rb +29 -6
  113. data/spec/cmd_init_spec.rb +281 -126
  114. data/spec/cmd_keystore_spec.rb +3 -3
  115. data/spec/cmd_link_spec.rb +98 -0
  116. data/spec/cmd_password_spec.rb +1 -1
  117. data/spec/cmd_setting_application_spec.rb +260 -0
  118. data/spec/cmd_setting_product_spec.rb +220 -0
  119. data/spec/cmd_status_spec.rb +223 -114
  120. data/spec/cmd_syncdown_spec.rb +115 -35
  121. data/spec/cmd_syncup_spec.rb +68 -15
  122. data/spec/cmd_usage_spec.rb +35 -8
  123. data/spec/fixtures/dumped_config +6 -4
  124. data/spec/fixtures/gateway_resource_files/resources.notyaml +12 -0
  125. data/spec/fixtures/gateway_resource_files/resources.yaml +13 -0
  126. data/spec/fixtures/gateway_resource_files/resources_invalid.yaml +13 -0
  127. data/spec/fixtures/mrmuranorc_deleted_bob +0 -2
  128. data/spec/fixtures/product_spec_files/lightbulb.yaml +20 -13
  129. data/spec/fixtures/{syncable_content → syncable_conflict}/services/devdata.lua +1 -1
  130. data/spec/fixtures/{syncable_content → syncable_conflict}/services/timers.lua +0 -0
  131. data/spec/spec_helper.rb +5 -0
  132. metadata +262 -171
  133. data/bin/mr +0 -8
  134. data/lib/MrMurano/Product-1P-Device.rb +0 -145
  135. data/lib/MrMurano/Product-Resources.rb +0 -205
  136. data/lib/MrMurano/Product.rb +0 -358
  137. data/lib/MrMurano/Solution-Cors.rb +0 -47
  138. data/lib/MrMurano/Solution-Endpoint.rb +0 -191
  139. data/lib/MrMurano/Solution-File.rb +0 -166
  140. data/lib/MrMurano/commands/assign.rb +0 -57
  141. data/lib/MrMurano/commands/businessList.rb +0 -45
  142. data/lib/MrMurano/commands/product.rb +0 -14
  143. data/lib/MrMurano/commands/productCreate.rb +0 -39
  144. data/lib/MrMurano/commands/productDelete.rb +0 -33
  145. data/lib/MrMurano/commands/productDevice.rb +0 -87
  146. data/lib/MrMurano/commands/productDeviceIdCmds.rb +0 -89
  147. data/lib/MrMurano/commands/productList.rb +0 -45
  148. data/lib/MrMurano/commands/productWrite.rb +0 -27
  149. data/lib/MrMurano/commands/solutionCreate.rb +0 -41
  150. data/lib/MrMurano/commands/solutionDelete.rb +0 -34
  151. data/lib/MrMurano/commands/solutionList.rb +0 -45
  152. data/spec/ProductBase_spec.rb +0 -113
  153. data/spec/ProductContent_spec.rb +0 -162
  154. data/spec/ProductResources_spec.rb +0 -329
  155. data/spec/Product_1P_Device_spec.rb +0 -202
  156. data/spec/Product_1P_RPC_spec.rb +0 -175
  157. data/spec/Product_spec.rb +0 -153
  158. data/spec/Solution-ServiceDevice_spec.rb +0 -176
  159. data/spec/cmd_assign_spec.rb +0 -51
@@ -0,0 +1,73 @@
1
+ # Last Modified: 2017.08.17 /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
+ module MrMurano
9
+ module SolutionId
10
+ INVALID_SID = '-1'
11
+ UNEXPECTED_TYPE_OR_ERROR_MSG = 'Unexpected result type or error: assuming empty instead'
12
+
13
+ attr_reader :sid
14
+ attr_reader :valid_sid
15
+
16
+ def init_sid!(sid=nil)
17
+ @valid_sid = false
18
+ unless defined?(@solntype) && @solntype
19
+ # Note that 'solution.id' isn't an actual config setting;
20
+ # see instead 'application.id' and 'product.id'. We just
21
+ # use 'solution.id' to indicate that the caller specified
22
+ # a solution ID explicitly (i.e., it's not from the $cfg).
23
+ raise 'Missing sid or class @solntype!?' if sid.to_s.empty?
24
+ @solntype = 'solution.id'
25
+ end
26
+ if sid
27
+ self.sid = sid
28
+ else
29
+ # Get the application.id or product.id.
30
+ self.sid = $cfg[@solntype]
31
+ end
32
+ # Maybe raise 'No application!' or 'No product!'.
33
+ return unless @sid.to_s.empty?
34
+ raise MrMurano::ConfigError.new("No #{/(.*).id/.match(@solntype)[1]} ID!")
35
+ end
36
+
37
+ def sid?
38
+ # The @sid should never be nil or empty, but let's at least check.
39
+ @sid != INVALID_SID && !@sid.to_s.empty?
40
+ end
41
+
42
+ def sid=(sid)
43
+ 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)
45
+ @sid = sid
46
+ # MAGIC_NUMBER: The 2nd element is the solution ID, e.g., solution/<sid>/...
47
+ raise "Unexpected @uriparts_sidex #{@uriparts_sidex}" unless @uriparts_sidex == 1
48
+ # We're called on initialize before @uriparts is built, so don't always do this.
49
+ @uriparts[@uriparts_sidex] = @sid if defined?(@uriparts)
50
+ end
51
+
52
+ def affirm_valid
53
+ @valid_sid = true
54
+ end
55
+
56
+ def valid_sid?
57
+ @valid_sid
58
+ end
59
+
60
+ # rubocop:disable Style/MethodName
61
+ def apiId
62
+ @sid
63
+ end
64
+
65
+ def endpoint(_path='')
66
+ # This is hopefully just a DEV error, and not something user will ever see!
67
+ return unless @uriparts[@uriparts_sidex] == INVALID_SID
68
+ error("Solution ID missing! Invalid ‘#{@solntype}’")
69
+ exit 2
70
+ end
71
+ end
72
+ end
73
+
@@ -0,0 +1,137 @@
1
+ # Last Modified: 2017.08.16 /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
+ module MrMurano
9
+ ## Track what things are syncable.
10
+ class SyncRoot
11
+ include Singleton
12
+
13
+ # A thing that is syncable.
14
+ Syncable = Struct.new(:name, :class, :type, :desc, :bydefault, :aliases) do
15
+ def opt_sym
16
+ name.tr('-', '_').to_sym
17
+ end
18
+ end
19
+
20
+ def initialize
21
+ @syncset = []
22
+ @synctypes = {}
23
+ end
24
+
25
+ ##
26
+ # Return the @syncset
27
+ # @return [Array<String>] array of Syncables
28
+ attr_reader :syncset
29
+
30
+ ##
31
+ # Add a new entry to syncable things
32
+ # @param name [String] The name to use for the long option
33
+ # @param klass [Class] The class to instanciate from
34
+ # @param type [String] Single letter for short option and status listing
35
+ # @param desc [String] Summary of what this syncs.
36
+ # @param bydefault [Boolean] Is this part of the default sync group
37
+ # @param aliases [Array] List of alternative CLI option names.
38
+ #
39
+ # @return [nil]
40
+ def add(name, klass, type, bydefault, aliases=[])
41
+ # 2017-06-20: Maybe possibly enforce unique name policy for --syncset options.
42
+ #@syncset.each do |a|
43
+ # if a.name == name.to_s
44
+ # msg = %{WARNING: SyncRoot.add called more than once for name "#{a.name}"}
45
+ # $stderr.puts HighLine.color(msg, :yellow)
46
+ # end
47
+ #end
48
+ # We should at least enforce a unique type policy,
49
+ # since the type is used to define the CLI option.
50
+ raise "Duplicate SyncRoot type specified: '#{type}'" if @synctypes[type]
51
+ @synctypes[type] = true
52
+ @syncset << Syncable.new(name.to_s, klass, type, klass.description, bydefault, aliases)
53
+ nil
54
+ end
55
+
56
+ ##
57
+ # Remove all syncables.
58
+ def reset
59
+ @syncset = []
60
+ @synctypes = {}
61
+ end
62
+
63
+ ##
64
+ # Get the list of default syncables.
65
+ # @return [Array<String>] array of names
66
+ def bydefault
67
+ @syncset.select(&:bydefault).map(&:name)
68
+ end
69
+
70
+ ##
71
+ # Iterate over all syncables
72
+ # @param block code to run on each
73
+ def each
74
+ @syncset.each { |a| yield a.name, a.type, a.class, a.desc }
75
+ end
76
+
77
+ ##
78
+ # Iterate over all syncables with option arguments.
79
+ # @param block code to run on each
80
+ def each_option
81
+ #@syncset.each { |a| yield "-#{a.type.downcase}", "--[no-]#{a.name}", a.desc }
82
+ @syncset.each { |a| yield "-#{a.type}", "--[no-]#{a.name}", a.desc }
83
+ end
84
+
85
+ ##
86
+ # Iterate over all syncables with option argument aliases.
87
+ # @param block code to run on each
88
+ def each_alias_opt
89
+ @syncset.each do |syncable|
90
+ syncable.aliases.each do |syn|
91
+ yield "--[no-]#{syn}", syncable.desc
92
+ end
93
+ end
94
+ end
95
+
96
+ def each_alias_sym
97
+ @syncset.each do |syncable|
98
+ syncable.aliases.each do |pseudonym|
99
+ yield pseudonym, pseudonym.tr('-', '_').to_sym, syncable.name, syncable.opt_sym
100
+ end
101
+ end
102
+ end
103
+
104
+ ##
105
+ # Iterate over just the selected syncables.
106
+ # @param opt [Hash{Symbol=>Boolean}] Options hash of which to select from
107
+ # @param block code to run on each
108
+ def each_filtered(opt)
109
+ check_same(opt)
110
+ @syncset.each do |a|
111
+ if opt[a.opt_sym] || opt[a.type.to_sym]
112
+ yield a.name, a.type, a.class, a.desc
113
+ end
114
+ end
115
+ end
116
+
117
+ ## Adjust options based on all or none
118
+ # If none are selected, select the bydefault ones.
119
+ #
120
+ # @param opt [Hash{Symbol=>Boolean}] Options hash of which to select from
121
+ #
122
+ # @return [nil]
123
+ def check_same(opt)
124
+ if opt[:all]
125
+ @syncset.each { |a| opt[a.name.to_sym] = true }
126
+ else
127
+ any = @syncset.select { |a| opt[a.opt_sym] || opt[a.type.to_sym] }
128
+ if any.empty?
129
+ bydef = $cfg['sync.bydefault'].split
130
+ @syncset.select { |a| bydef.include? a.name }.each { |a| opt[a.name.to_sym] = true }
131
+ end
132
+ end
133
+ nil
134
+ end
135
+ end
136
+ end
137
+
@@ -1,121 +1,60 @@
1
+ # Last Modified: 2017.08.18 /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
+ # FIXME/MAYBE: Fix semicolon usage.
9
+ # rubocop:disable Style/Semicolon
10
+
11
+ require 'inflecto'
12
+ require 'open3'
1
13
  require 'pathname'
14
+ #require 'shellwords'
2
15
  require 'tempfile'
3
- require 'shellwords'
4
- require 'open3'
5
- require 'MrMurano/Config'
6
- require 'MrMurano/ProjectFile'
16
+ require 'MrMurano/progress'
17
+ require 'MrMurano/verbosing'
7
18
  require 'MrMurano/hash'
19
+ #require 'MrMurano/Config'
20
+ #require 'MrMurano/ProjectFile'
21
+ ##require 'MrMurano/SyncRoot'
8
22
 
9
23
  module MrMurano
10
- ## Track what things are syncable.
11
- class SyncRoot
12
- # A thing that is syncable.
13
- Syncable = Struct.new(:name, :class, :type, :desc, :bydefault) do
14
- end
15
-
16
- ##
17
- # Add a new entry to syncable things
18
- # @param name [String] The name to use for the long option
19
- # @param klass [Class] The class to instanciate from
20
- # @param type [String] Single letter for short option and status listing
21
- # @param desc [String] Summary of what this syncs.
22
- # @param bydefault [Boolean] Is this part of the default sync group
23
- #
24
- # @return [nil]
25
- def self.add(name, klass, type, desc, bydefault=false)
26
- @@syncset = [] unless defined?(@@syncset)
27
- @@syncset << Syncable.new(name.to_s, klass, type, desc, bydefault)
28
- nil
29
- end
30
-
31
- ##
32
- # Remove all syncables.
33
- def self.reset()
34
- @@syncset = []
35
- end
36
-
37
- ##
38
- # Get the list of default syncables.
39
- # @return [Array<String>] array of names
40
- def self.bydefault
41
- @@syncset.select{|a| a.bydefault }.map{|a| a.name}
42
- end
43
-
44
- ##
45
- # Iterate over all syncables
46
- # @param block code to run on each
47
- def self.each(&block)
48
- @@syncset.each{|a| yield a.name, a.type, a.class }
49
- end
50
-
51
- ##
52
- # Iterate over all syncables with option arguments.
53
- # @param block code to run on each
54
- def self.each_option(&block)
55
- @@syncset.each{|a| yield "-#{a.type.downcase}", "--[no-]#{a.name}", a.desc}
56
- end
57
-
58
- ##
59
- # Iterate over just the selected syncables.
60
- # @param opt [Hash{Symbol=>Boolean}] Options hash of which to select from
61
- # @param block code to run on each
62
- def self.each_filtered(opt, &block)
63
- self.checkSAME(opt)
64
- @@syncset.each do |a|
65
- if opt[a.name.to_sym] or opt[a.type.to_sym] then
66
- yield a.name, a.type, a.class
67
- end
68
- end
69
- end
70
-
71
- ## Adjust options based on all or none
72
- # If none are selected, select the bydefault ones.
73
- #
74
- # @param opt [Hash{Symbol=>Boolean}] Options hash of which to select from
75
- #
76
- # @return [nil]
77
- def self.checkSAME(opt)
78
- if opt[:all] then
79
- @@syncset.each {|a| opt[a.name.to_sym] = true }
80
- else
81
- any = @@syncset.select {|a| opt[a.name.to_sym] or opt[a.type.to_sym]}
82
- if any.empty? then
83
- bydef = $cfg['sync.bydefault'].split
84
- @@syncset.select{|a| bydef.include? a.name }.each{|a| opt[a.name.to_sym] = true}
85
- end
86
- end
87
-
88
- nil
89
- end
90
- end
91
-
92
24
  ## The functionality of a Syncable thing.
93
25
  #
94
26
  # This provides the logic for computing what things have changed, and pushing and
95
27
  # pulling those things.
96
28
  #
97
29
  module SyncUpDown
98
-
99
- # This is one item that can be synced
30
+ # This is one item that can be synced.
100
31
  class Item
101
- # @return [String] The name of this item
32
+ # @return [String] The name of this item.
102
33
  attr_accessor :name
103
- # @return [Pathname] Where this item lives
34
+ # @return [Pathname] Where this item lives.
104
35
  attr_accessor :local_path
105
- # ??? what is this?
36
+ # FIXME/EXPLAIN: ??? what is this?
106
37
  attr_accessor :id
107
- # @return [String] The lua code for this item. (not all items use this.)
38
+ # @return [String] The Lua code for this item. (not all items use this.)
108
39
  attr_accessor :script
109
- # @return [Integer] The line in #local_path where this #script starts
40
+ # @return [Integer] The line in #local_path where this #script starts.
110
41
  attr_accessor :line
111
- # @return [Integer] The line in #local_path where this #script ends
42
+ # @return [Integer] The line in #local_path where this #script ends.
112
43
  attr_accessor :line_end
113
- # @return [String] If requested, the diff output
44
+ # @return [String] If requested, the diff output.
114
45
  attr_accessor :diff
115
46
  # @return [Boolean] When filtering, did this item pass.
116
47
  attr_accessor :selected
117
- # ???? what is this?
48
+ # @return [String] The constructed name used to match local items to remote items.
118
49
  attr_accessor :synckey
50
+ # @return [String] The syncable type.
51
+ attr_accessor :synctype
52
+ # @return [String] For device2, the event type.
53
+ attr_accessor :type
54
+ # @return [String] The updated_at time from the server is used to detect changes.
55
+ attr_accessor :updated_at
56
+ # @return [Integer] Positive if multiple conflicting files found for same item.
57
+ attr_accessor :dup_count
119
58
 
120
59
  # Initialize a new Item with a few, or all, attributes.
121
60
  # @param hsh [Hash{Symbol=>Object}, Item] Initial values
@@ -126,17 +65,17 @@ module MrMurano
126
65
  # item = Item.new(:name => 'get')
127
66
  # Item.new(item)
128
67
  def initialize(hsh={})
129
- hsh.each_pair{|k,v| self[k] = v}
68
+ hsh.each_pair { |k, v| self[k] = v }
130
69
  end
131
70
 
132
71
  def as_inst(key)
133
72
  return key if key.to_s[0] == '@'
134
- return "@#{key}"
73
+ "@#{key}"
135
74
  end
136
75
  private :as_inst
137
76
  def as_sym(key)
138
77
  return key.to_sym if key.to_s[0] != '@'
139
- return key.to_s[1..-1].to_sym
78
+ key.to_s[1..-1].to_sym
140
79
  end
141
80
  private :as_sym
142
81
 
@@ -150,7 +89,7 @@ module MrMurano
150
89
  # Set attribute as if this was a Hash
151
90
  # @param key [String,Symbol] attribute name
152
91
  # @param value [Object] value to set
153
- def []=(key,value)
92
+ def []=(key, value)
154
93
  public_send("#{key}=", value)
155
94
  end
156
95
 
@@ -164,14 +103,14 @@ module MrMurano
164
103
 
165
104
  # @return [Hash{Symbol=>Object}] A hash that represents this Item
166
105
  def to_h
167
- Hash[ instance_variables.map{|k| [ as_sym(k), instance_variable_get(k)]} ]
106
+ Hash[instance_variables.map { |k| [as_sym(k), instance_variable_get(k)] }]
168
107
  end
169
108
 
170
109
  # Adds the contents of item to self.
171
110
  # @param item [Item,Hash] Stuff to merge
172
111
  # @return [Item] ourself
173
112
  def merge!(item)
174
- item.each_pair{|k,v| self[k] = v}
113
+ item.each_pair { |k, v| self[k] = v }
175
114
  self
176
115
  end
177
116
 
@@ -186,7 +125,7 @@ module MrMurano
186
125
  # @yieldparam key [Symbol] The name of the key
187
126
  # @yieldparam value [Object] The value for that key
188
127
  # @return [Item]
189
- def each_pair(&block)
128
+ def each_pair
190
129
  instance_variables.each do |key|
191
130
  yield as_sym(key), instance_variable_get(key)
192
131
  end
@@ -198,7 +137,7 @@ module MrMurano
198
137
  # @yieldparam value [Object] The value for that key
199
138
  # @yieldreturn [Boolean] True to delete this key
200
139
  # @return [Item] Ourself.
201
- def reject!(&block)
140
+ def reject!(&_block)
202
141
  instance_variables.each do |key|
203
142
  drop = yield as_sym(key), instance_variable_get(key)
204
143
  delete(key) if drop
@@ -217,10 +156,12 @@ module MrMurano
217
156
 
218
157
  # For unit testing.
219
158
  include Comparable
220
- def <=>(anOther)
221
- self.to_h <=> anOther.to_h
159
+ def <=>(other)
160
+ # rubocop:disable Style/RedundantSelf: Redundant self detected.
161
+ # MAYBE/2017-07-18: Permanently disable Style/RedundantSelf?
162
+ self.to_h <=> other.to_h
222
163
  end
223
- end
164
+ end # MrMurano::SyncUpDown::Item
224
165
 
225
166
  #######################################################################
226
167
  # Methods that must be overridden
@@ -230,8 +171,8 @@ module MrMurano
230
171
  #
231
172
  # Children objects Must override this
232
173
  #
233
- # @return [Array] of Hashes of item details
234
- def list()
174
+ # @return [Array<Item>] of item details
175
+ def list
235
176
  []
236
177
  end
237
178
 
@@ -240,9 +181,9 @@ module MrMurano
240
181
  # Children objects Must override this
241
182
  #
242
183
  # @param itemkey [String] The identifying key for this item
243
- def remove(itemkey)
184
+ def remove(_itemkey)
244
185
  # :nocov:
245
- raise "Forgotten implementation"
186
+ raise 'Forgotten implementation'
246
187
  # :nocov:
247
188
  end
248
189
 
@@ -253,9 +194,9 @@ module MrMurano
253
194
  # @param src [Pathname] Full path of where to upload from
254
195
  # @param item [Hash] The item details to upload
255
196
  # @param modify [Bool] True if item exists already and this is changing it
256
- def upload(src, item, modify)
197
+ def upload(_src, _item, _modify)
257
198
  # :nocov:
258
- raise "Forgotten implementation"
199
+ raise 'Forgotten implementation'
259
200
  # :nocov:
260
201
  end
261
202
 
@@ -264,7 +205,7 @@ module MrMurano
264
205
  #
265
206
  # Children objects must override this
266
207
  #
267
- def docmp(itemA, itemB)
208
+ def docmp(_item_a, _item_b)
268
209
  true
269
210
  end
270
211
 
@@ -272,7 +213,7 @@ module MrMurano
272
213
  #######################################################################
273
214
 
274
215
  #######################################################################
275
- # Methods that could be overriden
216
+ # Methods that could be overridden
276
217
 
277
218
  ##
278
219
  # Compute a remote item hash from the local path
@@ -282,13 +223,13 @@ module MrMurano
282
223
  # @param root [Pathname,String] Root path for this resource type from config files
283
224
  # @param path [Pathname,String] Path to local item
284
225
  # @return [Item] hash of the details for the remote item for this path
285
- def toRemoteItem(root, path)
226
+ def to_remote_item(root, path)
286
227
  # This mess brought to you by Windows short path names.
287
228
  path = Dir.glob(path.to_s).first
288
229
  root = Dir.glob(root.to_s).first
289
230
  path = Pathname.new(path)
290
231
  root = Pathname.new(root)
291
- Item.new(:name => path.realpath.relative_path_from(root.realpath).to_s)
232
+ Item.new(name: path.realpath.relative_path_from(root.realpath).to_s)
292
233
  end
293
234
 
294
235
  ##
@@ -318,7 +259,7 @@ module MrMurano
318
259
  itemkey = @itemkey.to_sym
319
260
  name = tolocalname(item, itemkey)
320
261
  raise "Bad key(#{itemkey}) for #{item}" if name.nil?
321
- name = Pathname.new(name) unless name.kind_of? Pathname
262
+ name = Pathname.new(name) unless name.is_a? Pathname
322
263
  name = name.relative_path_from(Pathname.new('/')) if name.absolute?
323
264
  into + name
324
265
  end
@@ -332,7 +273,7 @@ module MrMurano
332
273
  # @param item [Item] Item to be checked
333
274
  # @param pattern [String] pattern to check with
334
275
  # @return [Bool] true or false
335
- def match(item, pattern)
276
+ def match(_item, _pattern)
336
277
  false
337
278
  end
338
279
 
@@ -354,13 +295,13 @@ module MrMurano
354
295
  # @param local [Pathname] Full path of where to download to
355
296
  # @param item [Item] The item to download
356
297
  def download(local, item)
357
- # if item[:bundled] then
298
+ # if item[:bundled]
358
299
  # warning "Not downloading into bundled item #{synckey(item)}"
359
300
  # return
360
301
  # end
361
302
  local.dirname.mkpath
362
303
  id = item[@itemkey.to_sym]
363
- if id.nil? then
304
+ if id.nil?
364
305
  debug "!!! Missing '#{@itemkey}', using :id instead!"
365
306
  debug ":id => #{item[:id]}"
366
307
  id = item[:id]
@@ -371,6 +312,74 @@ module MrMurano
371
312
  io.write chunk
372
313
  end
373
314
  end
315
+ update_mtime(local, item)
316
+ end
317
+
318
+ def diff_download(tmp_path, merged)
319
+ download(tmp_path, merged)
320
+ end
321
+
322
+ ## Give the local file the same timestamp as the remote, because diff.
323
+ #
324
+ # @param local [Pathname] Full path of where to download to
325
+ # @param item [Item] The item to download
326
+ def update_mtime(local, item)
327
+ # FIXME/MUR-XXXX: Ideally, server should use a hash we can compare.
328
+ # For now, we use the sometimes set :updated_at value.
329
+ # FIXME/EXPLAIN/2017-06-23: Why is :updated_at sometimes not set?
330
+ # (See more comments, below.)
331
+ return unless item[:updated_at]
332
+
333
+ mod_time = item[:updated_at]
334
+ mod_time = DateTime.parse(mod_time).to_time unless mod_time.is_a?(Time)
335
+ begin
336
+ FileUtils.touch([local.to_path], mtime: mod_time)
337
+ rescue Errno::EACCES => err
338
+ # This happens on Windows...
339
+ require 'rbconfig'
340
+ # Check the platform, e.g., "linux-gnu", or other.
341
+ is_windows = (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
342
+ unless is_windows
343
+ msg = 'Unexpected: touch failed on non-Windows machine'
344
+ $stderr.puts("#{msg} / host_os: #{RbConfig::CONFIG['host_os']} / err: #{err}")
345
+ end
346
+
347
+ # 2017-07-13: Nor does ctime work.
348
+ # Errno::EACCES:
349
+ # Permission denied @ utime_failed -
350
+ # C:/Users/ADMINI~1/AppData/Local/Temp/2/one.lua_remote_20170714-1856-by2nzk.lua
351
+ #File.utime(mod_time, mod_time, local.to_path)
352
+
353
+ # 2017-07-14: So this probably fails, too...
354
+ #FileUtils.touch [local.to_path,], :ctime => mod_time
355
+
356
+ # MAYBE/2017-07-14: How to make diff work on Windows?
357
+ # Would need to store timestamp in metafile?
358
+
359
+ # FIXME/EXPLAIN/2017-06-23: Why is :updated_at sometimes not set?
360
+ # And why have I only triggered this from ./spec/cmd_syncdown_spec.rb ?
361
+ # (Probably because nothing else makes routes or files?)
362
+ # Here are the items in question:
363
+ #
364
+ # Happens to each of the MrMurano::Webservice::Endpoint::RouteItem's:
365
+ #
366
+ # <MrMurano::Webservice::Endpoint::RouteItem:0x007fe719cb6300
367
+ # @id="QeRq21Cfij",
368
+ # @method="delete",
369
+ # @path="/api/fire/{code}",
370
+ # @content_type="application/json",
371
+ # @script="--#ENDPOINT delete /api/fire/{code}\nreturn 'ok'\n\n-- vim: set ai sw=2 ts=2 :\n",
372
+ # @use_basic_auth=false,
373
+ # @synckey="DELETE_/api/fire/{code}">
374
+ #
375
+ # Happens to each of the MrMurano::Webservice::File::FileItem's:
376
+ #
377
+ # <MrMurano::Webservice::File::FileItem:0x007fe71a44a8f0
378
+ # @path="/",
379
+ # @mime_type="text/html",
380
+ # @checksum="da39a3ee5e6b4b0d3255bfef95601890afd80709",
381
+ # @synckey="/">
382
+ end
374
383
  end
375
384
 
376
385
  ## Remove local reference of item
@@ -380,81 +389,152 @@ module MrMurano
380
389
  #
381
390
  # @param dest [Pathname] Full path of item to be removed
382
391
  # @param item [Item] Full details of item to be removed
383
- def removelocal(dest, item)
384
- dest.unlink
392
+ def removelocal(dest, _item)
393
+ dest.unlink if dest.exist?
394
+ end
395
+
396
+ def syncup_before
397
+ syncable_validate_sid
398
+ end
399
+
400
+ def syncup_after
401
+ end
402
+
403
+ def syncdown_before
404
+ syncable_validate_sid
405
+ end
406
+
407
+ def syncdown_after(_local)
408
+ end
409
+
410
+ def diff_item_write(io, merged, _local, _remote)
411
+ io << merged[:local_path].read
385
412
  end
386
413
 
387
414
  #
388
415
  #######################################################################
389
416
 
390
-
391
- # So, for bundles this needs to look at all the places and build up the merged
392
- # stack of local items.
417
+ # So, for bundles this needs to look at all the places
418
+ # and build up the merged stack of local items.
393
419
  #
394
- # Which means it needs the from to be split into the base and the sub so we can
395
- # inject bundle directories.
420
+ # Which means it needs the from to be split into the base
421
+ # and the sub so we can inject bundle directories.
396
422
 
397
423
  ##
398
424
  # Get a list of local items.
399
425
  #
400
- # Children should never need to override this. Instead they should override
401
- # #localitems
426
+ # Children should never need to override this.
427
+ # Instead they should override #localitems.
402
428
  #
403
429
  # This collects items in the project and all bundles.
404
430
  # @return [Array<Item>] items found
405
- def locallist()
406
- # so. if @locationbase/bundles exists
407
- # gather and merge: @locationbase/bundles/*/@location
408
- # then merge @locationbase/@location
409
- #
410
-
411
- # bundleDir = $cfg['location.bundles'] or 'bundles'
412
- # bundleDir = 'bundles' if bundleDir.nil?
431
+ #
432
+ # 2017-07-02: [lb] removed this commented-out code from the locallist
433
+ # body. I think it's for older Solutionfiles, like 0.2.0 and 0.3.0.
434
+ #def locallist
435
+ # # so. if @locationbase/bundles exists
436
+ # # gather and merge: @locationbase/bundles/*/@location
437
+ # # then merge @locationbase/@location
438
+ # #
439
+ # bundleDir = $cfg['location.bundles'] or 'bundles'
440
+ # bundleDir = 'bundles' if bundleDir.nil?
441
+ # items = {}
442
+ # if (@locationbase + bundleDir).directory?
443
+ # (@locationbase + bundleDir).children.sort.each do |bndl|
444
+ # if (bndl + @location).exist?
445
+ # verbose("Loading from bundle #{bndl.basename}")
446
+ # bitems = localitems(bndl + @location)
447
+ # bitems.map!{|b| b[:bundled] = true; b} # mark items from bundles.
448
+ # # use synckey for quicker merging.
449
+ # bitems.each { |b| items[synckey(b)] = b }
450
+ # end
451
+ # end
452
+ # end
453
+ #end
454
+ #
455
+ def locallist(skip_warn: false)
413
456
  items = {}
414
- # if (@locationbase + bundleDir).directory? then
415
- # (@locationbase + bundleDir).children.sort.each do |bndl|
416
- # if (bndl + @location).exist? then
417
- # verbose("Loading from bundle #{bndl.basename}")
418
- # bitems = localitems(bndl + @location)
419
- # bitems.map!{|b| b[:bundled] = true; b} # mark items from bundles.
420
- #
421
- #
422
- # # use synckey for quicker merging.
423
- # bitems.each { |b| items[synckey(b)] = b }
424
- # end
425
- # end
426
- # end
427
- if location.exist? then
457
+ if location.exist?
458
+ # Get a list of SyncUpDown::Item's, or a class derived thereof.
428
459
  bitems = localitems(location)
429
- # use synckey for quicker merging.
430
- bitems.each { |b| items[synckey(b)] = b }
431
- else
432
- warning "Skipping missing location #{location}"
460
+ # 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
+ #bitems.each { |b| items[synckey(b)] = b }
464
+ warns = {}
465
+ bitems.each do |item|
466
+ 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)
482
+ 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
488
+ else
489
+ items[skey] = item
490
+ end
491
+ end
492
+ elsif !skip_warn
493
+ @missing_complaints = [] unless defined?(@missing_complaints)
494
+ unless @missing_complaints.include?(location)
495
+ # MEH/2017-07-31: This message is a little misleading on syncdown,
496
+ # e.g., in rspec ./spec/cmd_syncdown_spec.rb, one test blows away
497
+ # 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)
499
+ # but then later in the syncdown, that directory and file gets created.
500
+ msg = "Skipping missing location ‘#{location}’"
501
+ unless self.class.description.to_s.empty?
502
+ msg += " (#{Inflecto.pluralize(self.class.description)})"
503
+ end
504
+ warning(msg)
505
+ @missing_complaints << location
506
+ end
433
507
  end
434
-
435
508
  items.values
436
509
  end
437
510
 
511
+ def resurrect_undeletables(localbox, _therebox)
512
+ # It's up to the Syncables to implement this, if they care.
513
+ localbox
514
+ end
515
+
438
516
  ##
439
517
  # Get the full path for the local versions
440
518
  # @return [Pathname] Location for local items
441
519
  def location
442
- raise "Missing @project_section" if @project_section.nil?
520
+ raise 'Missing @project_section' if @project_section.nil?
443
521
  Pathname.new($cfg['location.base']) + $project["#{@project_section}.location"]
444
522
  end
445
523
 
446
524
  ##
447
525
  # Returns array of globs to search for files
448
526
  # @return [Array<String>] of Strings that are globs
527
+ # rubocop:disable Style/MethodName: Use snake_case for method names.
528
+ # MAYBE/2017-07-18: Rename this. Beware the config has a related keyname.
449
529
  def searchFor
450
- raise "Missing @project_section" if @project_section.nil?
530
+ raise 'Missing @project_section' if @project_section.nil?
451
531
  $project["#{@project_section}.include"]
452
532
  end
453
533
 
454
534
  ## Returns array of globs of files to ignore
455
535
  # @return [Array<String>] of Strings that are globs
456
536
  def ignoring
457
- raise "Missing @project_section" if @project_section.nil?
537
+ raise 'Missing @project_section' if @project_section.nil?
458
538
  $project["#{@project_section}.exclude"]
459
539
  end
460
540
 
@@ -468,86 +548,141 @@ module MrMurano
468
548
  # @return [Array<Item>] Items found
469
549
  def localitems(from)
470
550
  # TODO: Profile this.
471
- debug "#{self.class.to_s}: Getting local items from: #{from}"
472
- searchIn = from.to_s
473
- sf = searchFor.map{|i| ::File.join(searchIn, i)}
474
- debug "#{self.class.to_s}: Globs: #{sf}"
475
- Dir[*sf].flatten.compact.reject do |p|
476
- ::File.directory?(p) or ignoring.any? do |i|
477
- ::File.fnmatch(i,p)
551
+ debug "#{self.class}: Getting local items from:\n #{from}"
552
+ search_in = from.to_s
553
+ sf = searchFor.map { |i| ::File.join(search_in, i) }
554
+ debug "#{self.class}: Globs:\n #{sf.join("\n ")}"
555
+ # 2017-07-27: Add uniq to cull duplicate entries that globbing
556
+ # all the ways might produce, otherwise status/sync/diff complain
557
+ # about duplicate resources. I [lb] think this problem has existed
558
+ # but was exacerbated by the change to support sub-directory scripts
559
+ # (Nested Lua support).
560
+ items = Dir[*sf].uniq.flatten.compact.reject do |path|
561
+ if ::File.directory?(path)
562
+ true
563
+ else
564
+ ignoring.any? { |pattern| self.ignore?(path, pattern) }
478
565
  end
479
- end.map do |path|
480
- path = Pathname.new(path).realpath
481
- item = toRemoteItem(from, path)
482
- if item.kind_of?(Array) then
483
- item.compact.map{|i| i[:local_path] = path; i}
484
- elsif not item.nil? then
485
- item[:local_path] = path
566
+ end
567
+ items = items.map do |path|
568
+ # Do not resolve symlinks, just relative paths (. and ..),
569
+ # otherwise it makes nested Lua support tricky, because
570
+ # symlinks might be outside the root item path, and then
571
+ # the nested Lua path looks like ".......some_dir/some_item".
572
+ if $cfg['modules.no-nesting']
573
+ rpath = Pathname.new(path).realpath
574
+ else
575
+ rpath = Pathname.new(path).expand_path
576
+ end
577
+ item = to_remote_item(from, rpath)
578
+ if item.is_a?(Array)
579
+ item.compact.map { |i| i[:local_path] = rpath; i }
580
+ elsif !item.nil?
581
+ item[:local_path] = rpath
486
582
  item
487
583
  end
488
- end.flatten.compact
584
+ end
585
+ #items = items.flatten.compact.sort_by!(&:local_path)
586
+ #debug "#{self.class}: items:\n #{items.map(&:local_path).join("\n ")}"
587
+ items = items.flatten.compact.sort_by { |it| it[:local_path] }
588
+ debug "#{self.class}: items:\n #{items.map { |it| it[:local_path] }.join("\n ")}"
589
+ sort_by_name(items)
590
+ end
591
+
592
+ def ignore?(path, pattern)
593
+ # 2017-08-18: [lb] not sure this block should be disabled for no-nesting.
594
+ # The block *was* added for Nested Lua support. But I think it was
595
+ # more necessary because modules.include is now '**/*.lua', not '*/*.lua'.
596
+ # Or maybe this block was because we now use expand_path, not realpath.
597
+ if !$cfg['modules.no-nesting'] && pattern.start_with?('**/')
598
+ # E.g., '**/.*' or '**/*'
599
+ dirname = File.dirname(path)
600
+ return true if ['.', ::File::ALT_SEPARATOR, ::File::SEPARATOR].include?(dirname)
601
+ # There's at least one ancestor directory.
602
+ # Remove the '**', which ::File.fnmatch doesn't recognize, and the path delimiter.
603
+ # 2017-08-08: Why does Rubocop not follow Style/RegexpLiteral here?
604
+ #pattern = pattern.gsub(/^\*\*\//, '')
605
+ pattern = pattern.gsub(%r{^\*\*\/}, '')
606
+ end
607
+
608
+ ::File.fnmatch(pattern, path)
489
609
  end
490
610
 
491
611
  #######################################################################
492
612
  # Methods that provide the core status/syncup/syncdown
493
613
 
494
- ##
495
- # Take a hash or something (a Commander::Command::Options) and return a hash
496
- #
497
- # @param hsh [Hash, Commander::Command::Options] Thing we want to be a Hash
498
- # @return [Hash] an actual Hash with default value of false
499
- def elevate_hash(hsh)
500
- # Commander::Command::Options stripped all of the methods from parent
501
- # objects. I have not nice thoughts about that.
502
- begin
503
- hsh = hsh.__hash__
504
- rescue NoMethodError
505
- # swallow this.
614
+ def sync_update_progress(msg)
615
+ if $cfg['tool.no-progress']
616
+ say(msg)
617
+ else
618
+ MrMurano::Verbose.verbose(msg + "\n")
506
619
  end
507
- # build a hash where the default is 'false' instead of 'nil'
508
- Hash.new(false).merge(Hash.transform_keys_to_symbols(hsh))
509
620
  end
510
- private :elevate_hash
511
621
 
512
622
  ## Make things in Murano look like local project
513
623
  #
514
624
  # This creates, uploads, and deletes things as needed up in Murano to match
515
625
  # what is in the local project directory.
516
626
  #
517
- # @param options [Hash, Commander::Command::Options] Options on opertation
627
+ # @param options [Hash, Commander::Command::Options] Options on operation
518
628
  # @param selected [Array<String>] Filters for _matcher
519
629
  def syncup(options={}, selected=[])
520
630
  options = elevate_hash(options)
521
- itemkey = @itemkey.to_sym
522
631
  options[:asdown] = false
632
+
633
+ num_synced = 0
634
+
635
+ syncup_before
636
+
523
637
  dt = status(options, selected)
638
+
524
639
  toadd = dt[:toadd]
525
640
  todel = dt[:todel]
526
641
  tomod = dt[:tomod]
527
642
 
528
- if options[:delete] then
529
- todel.each do |item|
530
- verbose "Removing item #{item[:synckey]}"
531
- unless $cfg['tool.dry'] then
532
- remove(item[itemkey])
533
- end
643
+ itemkey = @itemkey.to_sym
644
+ todel.each do |item|
645
+ syncup_item(item, options, :delete, 'Removing') do |aitem|
646
+ remove(aitem[itemkey])
647
+ num_synced += 1
534
648
  end
535
649
  end
536
- if options[:create] then
537
- toadd.each do |item|
538
- verbose "Adding item #{item[:synckey]}"
539
- unless $cfg['tool.dry'] then
540
- upload(item[:local_path], item.reject{|k,v| k==:local_path}, false)
541
- end
650
+ toadd.each do |item|
651
+ syncup_item(item, options, :create, 'Adding') do |aitem|
652
+ upload(aitem[:local_path], aitem.reject { |k, _v| k == :local_path }, false)
653
+ num_synced += 1
654
+ end
655
+ end
656
+ tomod.each do |item|
657
+ syncup_item(item, options, :update, 'Updating') do |aitem|
658
+ upload(aitem[:local_path], aitem.reject { |k, _v| k == :local_path }, true)
659
+ num_synced += 1
542
660
  end
543
661
  end
544
- if options[:update] then
545
- tomod.each do |item|
546
- verbose "Updating item #{item[:synckey]}"
547
- unless $cfg['tool.dry'] then
548
- upload(item[:local_path], item.reject{|k,v| k==:local_path}, true)
662
+
663
+ syncup_after
664
+
665
+ MrMurano::Verbose.whirly_stop(force: true)
666
+
667
+ num_synced
668
+ end
669
+
670
+ def syncup_item(item, options, action, verbage)
671
+ 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]}")
549
680
  end
550
681
  end
682
+ elsif $cfg['tool.verbose']
683
+ MrMurano::Verbose.whirly_interject do
684
+ say("--no-#{action}: Not #{verbage.downcase} item #{item[:synckey]}")
685
+ end
551
686
  end
552
687
  end
553
688
 
@@ -556,43 +691,60 @@ module MrMurano
556
691
  # This creates, downloads, and deletes things as needed up in the local project
557
692
  # directory to match what is in Murano.
558
693
  #
559
- # @param options [Hash, Commander::Command::Options] Options on opertation
694
+ # @param options [Hash, Commander::Command::Options] Options on operation
560
695
  # @param selected [Array<String>] Filters for _matcher
561
696
  def syncdown(options={}, selected=[])
562
697
  options = elevate_hash(options)
563
698
  options[:asdown] = true
699
+ options[:skip_missing_warning] = true
700
+
701
+ num_synced = 0
702
+
703
+ syncdown_before
704
+
564
705
  dt = status(options, selected)
565
- into = location ###
706
+
566
707
  toadd = dt[:toadd]
567
708
  todel = dt[:todel]
568
709
  tomod = dt[:tomod]
569
710
 
570
- if options[:delete] then
571
- todel.each do |item|
572
- verbose "Removing item #{item[:synckey]}"
573
- unless $cfg['tool.dry'] then
574
- dest = tolocalpath(into, item)
575
- removelocal(dest, item)
576
- end
711
+ into = location
712
+ todel.each do |item|
713
+ syncdown_item(item, into, options, :delete, 'Removing') do |dest, aitem|
714
+ removelocal(dest, aitem)
715
+ num_synced += 1
577
716
  end
578
717
  end
579
- if options[:create] then
580
- toadd.each do |item|
581
- verbose "Adding item #{item[:synckey]}"
582
- unless $cfg['tool.dry'] then
583
- dest = tolocalpath(into, item)
584
- download(dest, item)
585
- end
718
+ toadd.each do |item|
719
+ syncdown_item(item, into, options, :create, 'Adding') do |dest, aitem|
720
+ download(dest, aitem)
721
+ num_synced += 1
586
722
  end
587
723
  end
588
- if options[:update] then
589
- tomod.each do |item|
590
- verbose "Updating item #{item[:synckey]}"
591
- unless $cfg['tool.dry'] then
592
- dest = tolocalpath(into, item)
593
- download(dest, item)
594
- end
724
+ tomod.each do |item|
725
+ syncdown_item(item, into, options, :update, 'Updating') do |dest, aitem|
726
+ download(dest, aitem)
727
+ num_synced += 1
728
+ end
729
+ end
730
+ syncdown_after(into)
731
+
732
+ num_synced
733
+ end
734
+
735
+ def syncdown_item(item, into, options, action, verbage)
736
+ 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]}")
595
745
  end
746
+ elsif $cfg['tool.verbose']
747
+ say("--no-#{action}: Not #{verbage.downcase} item #{item[:synckey]}")
596
748
  end
597
749
  end
598
750
 
@@ -600,36 +752,75 @@ module MrMurano
600
752
  #
601
753
  # WARNING: This will download the remote item to do the diff.
602
754
  #
603
- # @param item [Item] The item to get a diff of
755
+ # @param merged [merged] The merged item to get a diff of
756
+ # @local local, unadulterated (non-merged) item
604
757
  # @return [String] The diff output
605
- def dodiff(item)
606
- trmt = Tempfile.new([tolocalname(item, @itemkey)+'_remote_', '.lua'])
607
- tlcl = Tempfile.new([tolocalname(item, @itemkey)+'_local_', '.lua'])
608
- if item.has_key? :script then
609
- Pathname.new(tlcl.path).open('wb') do |io|
610
- io << item[:script]
611
- end
612
- else
613
- Pathname.new(tlcl.path).open('wb') do |io|
614
- io << item[:local_path].read
758
+ def dodiff(merged, local, _there=nil, asdown=false)
759
+ trmt = Tempfile.new([tolocalname(merged, @itemkey) + '_remote_', '.lua'])
760
+ tlcl = Tempfile.new([tolocalname(merged, @itemkey) + '_local_', '.lua'])
761
+ Pathname.new(tlcl.path).open('wb') do |io|
762
+ if merged.key?(:script)
763
+ io << merged[:script]
764
+ else
765
+ # For most items, read the local file.
766
+ # For resources, it's a bit trickier.
767
+ # NOTE: This class adds a :selected key to the local item that we need
768
+ # to remove, since it's not part of the remote items that gets downloaded.
769
+ local = local.reject { |k, _v| k == :selected } unless local.nil?
770
+ diff_item_write(io, merged, local, nil)
615
771
  end
616
772
  end
617
- df = ""
773
+ stdout_and_stderr = ''
618
774
  begin
619
- download(Pathname.new(trmt.path), item)
775
+ tmp_path = Pathname.new(trmt.path)
776
+ diff_download(tmp_path, merged)
777
+
778
+ MrMurano::Verbose.whirly_stop
620
779
 
780
+ # 2017-07-03: No worries, Ruby 3.0 frozen string literals, cmd is a list.
621
781
  cmd = $cfg['diff.cmd'].shellsplit
622
- cmd << trmt.path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR)
623
- cmd << tlcl.path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR)
782
+ # ALT_SEPARATOR is the platform specific alternative separator,
783
+ # for Windows support.
784
+ remote_path = trmt.path.gsub(
785
+ ::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR
786
+ )
787
+ local_path = tlcl.path.gsub(
788
+ ::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR
789
+ )
790
+ if asdown
791
+ cmd << local_path
792
+ cmd << remote_path
793
+ else
794
+ cmd << remote_path
795
+ cmd << local_path
796
+ end
624
797
 
625
- df, _ = Open3.capture2e(*cmd)
798
+ stdout_and_stderr, _status = Open3.capture2e(*cmd)
799
+ # How important are the first two lines of the diff? E.g.,
800
+ # --- /tmp/raw_data_remote_20170718-20183-gdyeg9.lua 2017-07-18 13:13:13.864051905 -0500
801
+ # +++ /tmp/raw_data_local_20170718-20183-71o4me.lua 2017-07-18 13:13:14.520049397 -0500
802
+ # Seems like printing the path to a since-deleted temporary file is misleading.
803
+ if $cfg['diff.cmd'] == 'diff' || $cfg['diff.cmd'].start_with?('diff ')
804
+ lineno = 0
805
+ consise = stdout_and_stderr.lines.reject do |line|
806
+ lineno += 1
807
+ if lineno == 1 && line.start_with?('--- ')
808
+ true
809
+ elsif lineno == 2 && line.start_with?('+++ ')
810
+ true
811
+ else
812
+ false
813
+ end
814
+ end
815
+ stdout_and_stderr = consise.join
816
+ end
626
817
  ensure
627
818
  trmt.close
628
819
  trmt.unlink
629
820
  tlcl.close
630
821
  tlcl.unlink
631
822
  end
632
- df
823
+ stdout_and_stderr
633
824
  end
634
825
 
635
826
  ##
@@ -638,13 +829,13 @@ module MrMurano
638
829
  # @param patterns [Array<String>] Filters for _matcher
639
830
  def _matcher(items, patterns)
640
831
  items.map do |item|
641
- if patterns.empty? then
832
+ if patterns.empty?
642
833
  item[:selected] = true
643
834
  else
644
835
  item[:selected] = patterns.any? do |pattern|
645
- if pattern.to_s[0] == '#' then
836
+ if pattern.to_s[0] == '#'
646
837
  match(item, pattern)
647
- elsif item.local_path.nil? then
838
+ elsif !defined?(item.local_path) || item.local_path.nil?
648
839
  false
649
840
  else
650
841
  item[:local_path].fnmatch(pattern)
@@ -663,54 +854,173 @@ module MrMurano
663
854
  # @return [Hash{Symbol=>Array<Item>}] Items grouped by the action that should be taken
664
855
  def status(options={}, selected=[])
665
856
  options = elevate_hash(options)
666
- itemkey = @itemkey.to_sym
667
857
 
668
- there = _matcher(list(), selected) # Array<Item>
669
- here = _matcher(locallist(), selected) # Array<Item>
858
+ ret = filter_solution(options)
859
+ return ret unless ret.nil?
860
+
861
+ therebox, localbox = items_lists(options, selected)
862
+
863
+ toadd, todel = items_new_and_old(options, therebox, localbox)
864
+
865
+ tomod, unchg = items_mods_and_chgs(options, therebox, localbox)
866
+
867
+ if options[:unselected]
868
+ { toadd: toadd, todel: todel, tomod: tomod, unchg: unchg, skipd: [] }
869
+ else
870
+ {
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 },
875
+ skipd: [],
876
+ }
877
+ end
878
+ end
879
+
880
+ def filter_solution(options)
881
+ # Get the solution name from the config.
882
+ # Convert, e.g., application.id => application.name
883
+ soln_name = $cfg[@solntype.gsub(/(.*)\.id/, '\1.name')]
884
+ # Skip this syncable if the sid is not set, or if user wants to skip by solution.
885
+ skip_sol = false
886
+ if !sid? ||
887
+ (options[:type] == :application && @solntype != 'application.id') ||
888
+ (options[:type] == :product && @solntype != 'product.id')
889
+ skip_sol = true
890
+ else
891
+ tested = false
892
+ passed = false
893
+ if @solntype == 'application.id'
894
+ # elevate_hash magically makes the hash return false rather than nil
895
+ # on unknown keys, so preface with a key? guard.
896
+ if options.key?(:application) && !options[:application].to_s.empty?
897
+ if soln_name =~ /#{Regexp.escape(options[:application])}/i ||
898
+ sid =~ /#{Regexp.escape(options[:application])}/i
899
+ passed = true
900
+ end
901
+ tested = true
902
+ end
903
+ if options.key?(:application_id) && !options[:application_id].to_s.empty?
904
+ passed = true if options[:application_id] == sid
905
+ tested = true
906
+ end
907
+ if options.key?(:application_name) && !options[:application_name].to_s.empty?
908
+ passed = true if options[:application_name] == soln_name
909
+ tested = true
910
+ end
911
+ elsif @solntype == 'product.id'
912
+ if options.key?(:product) && !options[:product].to_s.empty?
913
+ if soln_name =~ /#{Regexp.escape(options[:product])}/i ||
914
+ sid =~ /#{Regexp.escape(options[:product])}/i
915
+ passed = true
916
+ end
917
+ tested = true
918
+ end
919
+ if options.key?(:product_id) && !options[:product_id].to_s.empty?
920
+ passed = true if options[:product_id] == sid
921
+ tested = true
922
+ end
923
+ if options.key?(:product_name) && !options[:product_name].to_s.empty?
924
+ passed = true if options[:product_name] == soln_name
925
+ tested = true
926
+ end
927
+ end
928
+ skip_sol = true if tested && !passed
929
+ end
930
+ return nil unless skip_sol
931
+ ret = { toadd: [], todel: [], tomod: [], unchg: [], skipd: [] }
932
+ ret[:skipd] << { synckey: self.class.description }
933
+ ret
934
+ end
935
+
936
+ def syncable_validate_sid
937
+ # 2017-07-02: Now that there are multiple solution types, and because
938
+ # SyncRoot.add is called on different classes that go with either or
939
+ # both products and applications, if a user only created one solution,
940
+ # then some syncables will have their sid set to -1, because there's
941
+ # not a corresponding solution in Murano.
942
+ raise 'Syncable missing sid or not valid_sid??!' unless sid?
943
+ end
944
+
945
+ def items_lists(options, selected)
946
+ # Fetch arrays of items there, and items here/local.
947
+ there = list
948
+ there = _matcher(there, selected)
949
+ local = locallist(skip_warn: options[:skip_missing_warning])
950
+ local = _matcher(local, selected)
670
951
 
671
952
  therebox = {}
672
953
  there.each do |item|
673
954
  item[:synckey] = synckey(item)
674
- therebox[ item[:synckey] ] = item
955
+ item[:synctype] = self.class.description
956
+ therebox[item[:synckey]] = item
675
957
  end
676
- herebox = {}
677
- here.each do |item|
678
- item[:synckey] = synckey(item)
679
- herebox[ item[:synckey] ] = item
958
+
959
+ localbox = {}
960
+ local.each do |item|
961
+ skey = synckey(item)
962
+ # 2017-07-02: Check for local duplicates.
963
+ skey += "-#{item[:dup_count]}" unless item[:dup_count].nil?
964
+ item[:synckey] = skey
965
+ item[:synctype] = self.class.description
966
+ localbox[item[:synckey]] = item
680
967
  end
681
- toadd = []
682
- todel = []
968
+
969
+ # Some items are considered "undeletable", meaning if a
970
+ # corresponding file does not exist locally, we assume
971
+ # it does but is just set to the empty string.
972
+ localbox = resurrect_undeletables(localbox, therebox)
973
+
974
+ [therebox, localbox]
975
+ end
976
+
977
+ def items_new_and_old(options, therebox, localbox)
978
+ if options[:asdown]
979
+ todel = (localbox.keys - therebox.keys).map { |key| localbox[key] }
980
+ toadd = (therebox.keys - localbox.keys).map { |key| therebox[key] }
981
+ else
982
+ toadd = (localbox.keys - therebox.keys).map { |key| localbox[key] }
983
+ todel = (therebox.keys - localbox.keys).map { |key| therebox[key] }
984
+ end
985
+ [sort_by_name(toadd), sort_by_name(todel)]
986
+ end
987
+
988
+ def items_mods_and_chgs(options, therebox, localbox)
683
989
  tomod = []
684
990
  unchg = []
685
- if options[:asdown] then
686
- todel = (herebox.keys - therebox.keys).map{|key| herebox[key] }
687
- toadd = (therebox.keys - herebox.keys).map{|key| therebox[key] }
688
- else
689
- toadd = (herebox.keys - therebox.keys).map{|key| herebox[key] }
690
- todel = (therebox.keys - herebox.keys).map{|key| therebox[key] }
691
- end
692
- (herebox.keys & therebox.keys).each do |key|
693
- # Want here to override there except for itemkey.
694
- mrg = herebox[key].reject{|k,v| k==itemkey}
695
- mrg = therebox[key].merge(mrg)
696
- if docmp(herebox[key], therebox[key]) then
697
- mrg[:diff] = dodiff(mrg.to_h) if options[:diff] and mrg[:selected]
991
+
992
+ (localbox.keys & therebox.keys).each do |key|
993
+ # Want 'local' to override 'there' except for itemkey.
994
+ if options[:asdown]
995
+ mrg = therebox[key].reject { |k, _v| k == @itemkey.to_sym }
996
+ mrg = localbox[key].merge(mrg)
997
+ else
998
+ mrg = localbox[key].reject { |k, _v| k == @itemkey.to_sym }
999
+ mrg = therebox[key].merge(mrg)
1000
+ end
1001
+
1002
+ if docmp(localbox[key], therebox[key])
1003
+ if options[:diff] && mrg[:selected]
1004
+ mrg[:diff] = dodiff(mrg.to_h, localbox[key], therebox[key], options[:asdown])
1005
+ end
698
1006
  tomod << mrg
699
1007
  else
700
1008
  unchg << mrg
701
1009
  end
702
1010
  end
703
- if options[:unselected] then
704
- { :toadd=>toadd, :todel=>todel, :tomod=>tomod, :unchg=>unchg }
1011
+ [sort_by_name(tomod), sort_by_name(unchg)]
1012
+ end
1013
+
1014
+ def sort_by_name(list)
1015
+ if list.any? && list.first.is_a?(Hash)
1016
+ # AFAIK, only SyncUpDown_spec.rb comes through here, because
1017
+ # it does not use SyncUpDown::Item but mocks its own items
1018
+ # using hashes (see calls to and_return). [lb]
1019
+ list.sort_by { |hsh| hsh[:name] }
705
1020
  else
706
- {
707
- :toadd=>toadd.select{|i| i[:selected]}.map{|i| i.delete(:selected); i},
708
- :todel=>todel.select{|i| i[:selected]}.map{|i| i.delete(:selected); i},
709
- :tomod=>tomod.select{|i| i[:selected]}.map{|i| i.delete(:selected); i},
710
- :unchg=>unchg.select{|i| i[:selected]}.map{|i| i.delete(:selected); i}
711
- }
1021
+ list.sort_by(&:name)
712
1022
  end
713
1023
  end
714
1024
  end
715
1025
  end
716
- # vim: set ai et sw=2 ts=2 :
1026
+