MuranoCLI 2.2.4 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.agignore +3 -0
- data/.gitignore +18 -1
- data/.rubocop.yml +222 -0
- data/.trustme.sh +185 -0
- data/.trustme.vim +24 -0
- data/Gemfile +23 -4
- data/LICENSE.txt +1 -1
- data/MuranoCLI.gemspec +43 -8
- data/README.markdown +9 -11
- data/Rakefile +187 -143
- data/TODO.taskpaper +2 -2
- data/bin/murano +51 -52
- data/docs/basic_example.rst +436 -0
- data/docs/completions/murano_completion-bash +3484 -0
- data/docs/demo.md +32 -32
- data/docs/develop.rst +391 -0
- data/lib/MrMurano.rb +21 -7
- data/lib/MrMurano/Account.rb +159 -174
- data/lib/MrMurano/Business.rb +381 -0
- data/lib/MrMurano/Config-Migrate.rb +32 -26
- data/lib/MrMurano/Config.rb +407 -128
- data/lib/MrMurano/Content.rb +191 -0
- data/lib/MrMurano/Gateway.rb +489 -0
- data/lib/MrMurano/Keystore.rb +48 -0
- data/lib/MrMurano/Passwords.rb +103 -0
- data/lib/MrMurano/ProjectFile.rb +121 -79
- data/lib/MrMurano/ReCommander.rb +114 -10
- data/lib/MrMurano/Setting.rb +90 -0
- data/lib/MrMurano/Solution-ServiceConfig.rb +89 -45
- data/lib/MrMurano/Solution-Services.rb +461 -166
- data/lib/MrMurano/Solution-Users.rb +70 -31
- data/lib/MrMurano/Solution.rb +372 -13
- data/lib/MrMurano/SolutionId.rb +73 -0
- data/lib/MrMurano/SyncRoot.rb +137 -0
- data/lib/MrMurano/SyncUpDown.rb +594 -284
- data/lib/MrMurano/Webservice-Cors.rb +71 -0
- data/lib/MrMurano/Webservice-Endpoint.rb +234 -0
- data/lib/MrMurano/Webservice-File.rb +193 -0
- data/lib/MrMurano/Webservice.rb +51 -0
- data/lib/MrMurano/commands.rb +18 -15
- data/lib/MrMurano/commands/business.rb +300 -6
- data/lib/MrMurano/commands/completion-bash.erb +166 -0
- data/lib/MrMurano/commands/{zshcomplete.erb → completion-zsh.erb} +0 -0
- data/lib/MrMurano/commands/completion.rb +76 -39
- data/lib/MrMurano/commands/config.rb +108 -44
- data/lib/MrMurano/commands/content.rb +115 -72
- data/lib/MrMurano/commands/cors.rb +29 -14
- data/lib/MrMurano/commands/devices.rb +286 -0
- data/lib/MrMurano/commands/domain.rb +52 -12
- data/lib/MrMurano/commands/gb.rb +24 -9
- data/lib/MrMurano/commands/globals.rb +64 -0
- data/lib/MrMurano/commands/init.rb +377 -155
- data/lib/MrMurano/commands/keystore.rb +92 -82
- data/lib/MrMurano/commands/link.rb +300 -0
- data/lib/MrMurano/commands/login.rb +74 -11
- data/lib/MrMurano/commands/logs.rb +63 -32
- data/lib/MrMurano/commands/mock.rb +57 -29
- data/lib/MrMurano/commands/password.rb +57 -39
- data/lib/MrMurano/commands/postgresql.rb +127 -94
- data/lib/MrMurano/commands/settings.rb +203 -0
- data/lib/MrMurano/commands/show.rb +79 -38
- data/lib/MrMurano/commands/solution.rb +423 -5
- data/lib/MrMurano/commands/solution_picker.rb +547 -0
- data/lib/MrMurano/commands/status.rb +195 -61
- data/lib/MrMurano/commands/sync.rb +78 -39
- data/lib/MrMurano/commands/timeseries.rb +71 -55
- data/lib/MrMurano/commands/tsdb.rb +113 -87
- data/lib/MrMurano/commands/usage.rb +57 -15
- data/lib/MrMurano/hash.rb +100 -10
- data/lib/MrMurano/http.rb +187 -43
- data/lib/MrMurano/makePretty.rb +16 -14
- data/lib/MrMurano/optparse.rb +2178 -0
- data/lib/MrMurano/progress.rb +138 -0
- data/lib/MrMurano/schema/resource-v1.0.0.yaml +32 -0
- data/lib/MrMurano/template/projectFile.murano.erb +16 -13
- data/lib/MrMurano/verbosing.rb +166 -29
- data/lib/MrMurano/version.rb +30 -1
- data/spec/Account-Passwords_spec.rb +21 -4
- data/spec/Account_spec.rb +69 -146
- data/spec/Business_spec.rb +290 -0
- data/spec/ConfigFile_spec.rb +1 -0
- data/spec/ConfigMigrate_spec.rb +12 -8
- data/spec/Config_spec.rb +40 -34
- data/spec/Content_spec.rb +363 -0
- data/spec/GatewayBase_spec.rb +54 -0
- data/spec/GatewayDevice_spec.rb +321 -0
- data/spec/GatewayResource_spec.rb +266 -0
- data/spec/GatewaySettings_spec.rb +120 -0
- data/spec/Http_spec.rb +18 -8
- data/spec/Mock_spec.rb +2 -2
- data/spec/ProjectFile_spec.rb +25 -14
- data/spec/Setting_spec.rb +110 -0
- data/spec/Solution-ServiceConfig_spec.rb +44 -5
- data/spec/Solution-ServiceEventHandler_spec.rb +23 -14
- data/spec/Solution-ServiceModules_spec.rb +47 -37
- data/spec/Solution-UsersRoles_spec.rb +10 -8
- data/spec/Solution_spec.rb +17 -8
- data/spec/SyncRoot_spec.rb +46 -20
- data/spec/SyncUpDown_spec.rb +437 -201
- data/spec/Verbosing_spec.rb +12 -4
- data/spec/{Solution-Cors_spec.rb → Webservice-Cors_spec.rb} +23 -20
- data/spec/{Solution-Endpoint_spec.rb → Webservice-Endpoint_spec.rb} +43 -41
- data/spec/{Solution-File_spec.rb → Webservice-File_spec.rb} +44 -33
- data/spec/Webservice-Setting_spec.rb +89 -0
- data/spec/_workspace.rb +4 -4
- data/spec/cmd_business_spec.rb +9 -4
- data/spec/cmd_common.rb +44 -1
- data/spec/cmd_content_spec.rb +43 -17
- data/spec/cmd_cors_spec.rb +4 -4
- data/spec/cmd_device_spec.rb +61 -16
- data/spec/cmd_domain_spec.rb +29 -6
- data/spec/cmd_init_spec.rb +281 -126
- data/spec/cmd_keystore_spec.rb +3 -3
- data/spec/cmd_link_spec.rb +98 -0
- data/spec/cmd_password_spec.rb +1 -1
- data/spec/cmd_setting_application_spec.rb +260 -0
- data/spec/cmd_setting_product_spec.rb +220 -0
- data/spec/cmd_status_spec.rb +223 -114
- data/spec/cmd_syncdown_spec.rb +115 -35
- data/spec/cmd_syncup_spec.rb +68 -15
- data/spec/cmd_usage_spec.rb +35 -8
- data/spec/fixtures/dumped_config +6 -4
- data/spec/fixtures/gateway_resource_files/resources.notyaml +12 -0
- data/spec/fixtures/gateway_resource_files/resources.yaml +13 -0
- data/spec/fixtures/gateway_resource_files/resources_invalid.yaml +13 -0
- data/spec/fixtures/mrmuranorc_deleted_bob +0 -2
- data/spec/fixtures/product_spec_files/lightbulb.yaml +20 -13
- data/spec/fixtures/{syncable_content → syncable_conflict}/services/devdata.lua +1 -1
- data/spec/fixtures/{syncable_content → syncable_conflict}/services/timers.lua +0 -0
- data/spec/spec_helper.rb +5 -0
- metadata +262 -171
- data/bin/mr +0 -8
- data/lib/MrMurano/Product-1P-Device.rb +0 -145
- data/lib/MrMurano/Product-Resources.rb +0 -205
- data/lib/MrMurano/Product.rb +0 -358
- data/lib/MrMurano/Solution-Cors.rb +0 -47
- data/lib/MrMurano/Solution-Endpoint.rb +0 -191
- data/lib/MrMurano/Solution-File.rb +0 -166
- data/lib/MrMurano/commands/assign.rb +0 -57
- data/lib/MrMurano/commands/businessList.rb +0 -45
- data/lib/MrMurano/commands/product.rb +0 -14
- data/lib/MrMurano/commands/productCreate.rb +0 -39
- data/lib/MrMurano/commands/productDelete.rb +0 -33
- data/lib/MrMurano/commands/productDevice.rb +0 -87
- data/lib/MrMurano/commands/productDeviceIdCmds.rb +0 -89
- data/lib/MrMurano/commands/productList.rb +0 -45
- data/lib/MrMurano/commands/productWrite.rb +0 -27
- data/lib/MrMurano/commands/solutionCreate.rb +0 -41
- data/lib/MrMurano/commands/solutionDelete.rb +0 -34
- data/lib/MrMurano/commands/solutionList.rb +0 -45
- data/spec/ProductBase_spec.rb +0 -113
- data/spec/ProductContent_spec.rb +0 -162
- data/spec/ProductResources_spec.rb +0 -329
- data/spec/Product_1P_Device_spec.rb +0 -202
- data/spec/Product_1P_RPC_spec.rb +0 -175
- data/spec/Product_spec.rb +0 -153
- data/spec/Solution-ServiceDevice_spec.rb +0 -176
- 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
|
+
|
data/lib/MrMurano/SyncUpDown.rb
CHANGED
@@ -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 '
|
4
|
-
require '
|
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
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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[
|
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
|
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!(&
|
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 <=>(
|
221
|
-
self
|
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
|
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(
|
184
|
+
def remove(_itemkey)
|
244
185
|
# :nocov:
|
245
|
-
raise
|
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(
|
197
|
+
def upload(_src, _item, _modify)
|
257
198
|
# :nocov:
|
258
|
-
raise
|
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(
|
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
|
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
|
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(:
|
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.
|
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(
|
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]
|
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?
|
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,
|
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
|
-
#
|
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
|
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.
|
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
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
#
|
412
|
-
#
|
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
|
-
|
415
|
-
#
|
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
|
-
#
|
430
|
-
|
431
|
-
|
432
|
-
|
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
|
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
|
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
|
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
|
472
|
-
|
473
|
-
sf = searchFor.map{|i| ::File.join(
|
474
|
-
debug "#{self.class
|
475
|
-
|
476
|
-
|
477
|
-
|
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
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
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
|
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
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
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
|
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
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
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
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
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
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
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
|
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
|
-
|
706
|
+
|
566
707
|
toadd = dt[:toadd]
|
567
708
|
todel = dt[:todel]
|
568
709
|
tomod = dt[:tomod]
|
569
710
|
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
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
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
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
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
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
|
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(
|
606
|
-
trmt = Tempfile.new([tolocalname(
|
607
|
-
tlcl = Tempfile.new([tolocalname(
|
608
|
-
|
609
|
-
|
610
|
-
io <<
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
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
|
-
|
773
|
+
stdout_and_stderr = ''
|
618
774
|
begin
|
619
|
-
|
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
|
-
|
623
|
-
|
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
|
-
|
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
|
-
|
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?
|
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] == '#'
|
836
|
+
if pattern.to_s[0] == '#'
|
646
837
|
match(item, pattern)
|
647
|
-
elsif item.local_path.nil?
|
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
|
-
|
669
|
-
|
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
|
-
|
955
|
+
item[:synctype] = self.class.description
|
956
|
+
therebox[item[:synckey]] = item
|
675
957
|
end
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
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
|
-
|
682
|
-
|
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
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
if docmp(
|
697
|
-
|
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
|
-
|
704
|
-
|
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
|
-
|
1026
|
+
|