shopify-cli 2.15.0 → 2.15.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/stale.yml +7 -2
  3. data/.vscode/settings.json +1 -2
  4. data/CHANGELOG.md +70 -19
  5. data/Gemfile +1 -0
  6. data/Gemfile.lock +39 -7
  7. data/Rakefile +48 -0
  8. data/ext/javy/hashes/javy-arm-macos-v0.3.0.gz.sha256 +1 -0
  9. data/ext/javy/hashes/javy-x86_64-linux-v0.3.0.gz.sha256 +1 -0
  10. data/ext/javy/hashes/javy-x86_64-macos-v0.3.0.gz.sha256 +1 -0
  11. data/ext/javy/hashes/javy-x86_64-windows-v0.3.0.gz.sha256 +1 -0
  12. data/ext/javy/version +1 -1
  13. data/ext/shopify-extensions/version +1 -1
  14. data/lib/project_types/extension/commands/check.rb +6 -1
  15. data/lib/project_types/extension/forms/questions/ask_template.rb +5 -8
  16. data/lib/project_types/extension/messages/messages.rb +11 -3
  17. data/lib/project_types/extension/models/development_server_requirements.rb +14 -7
  18. data/lib/project_types/extension/models/server_config/root.rb +2 -0
  19. data/lib/project_types/extension/models/specification_handlers/beacon_extension.rb +57 -0
  20. data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config.rb +33 -0
  21. data/lib/project_types/extension/models/specification_handlers/beacon_extension_utils/script_config_repository.rb +75 -0
  22. data/lib/project_types/extension/models/specification_handlers/checkout_ui_extension.rb +16 -1
  23. data/lib/project_types/extension/tasks/configure_options.rb +2 -1
  24. data/lib/project_types/extension/tasks/convert_server_config.rb +13 -2
  25. data/lib/project_types/extension/tasks/merge_server_config.rb +5 -2
  26. data/lib/project_types/script/cli.rb +1 -0
  27. data/lib/project_types/script/config/extension_points.yml +18 -0
  28. data/lib/project_types/script/layers/application/create_script.rb +14 -6
  29. data/lib/project_types/script/layers/infrastructure/errors.rb +17 -0
  30. data/lib/project_types/script/layers/infrastructure/languages/project_creator.rb +6 -21
  31. data/lib/project_types/script/layers/infrastructure/script_service.rb +2 -0
  32. data/lib/project_types/script/layers/infrastructure/sparse_checkout_details.rb +35 -0
  33. data/lib/project_types/script/messages/messages.rb +3 -0
  34. data/lib/project_types/script/ui/error_handler.rb +11 -0
  35. data/lib/project_types/theme/cli.rb +1 -0
  36. data/lib/project_types/theme/commands/check.rb +4 -1
  37. data/lib/project_types/theme/commands/open.rb +2 -2
  38. data/lib/project_types/theme/commands/push.rb +1 -3
  39. data/lib/project_types/theme/commands/serve.rb +1 -0
  40. data/lib/project_types/theme/commands/share.rb +56 -0
  41. data/lib/project_types/theme/messages/messages.rb +71 -11
  42. data/lib/shopify_cli/changelog.rb +148 -0
  43. data/lib/shopify_cli/command.rb +7 -0
  44. data/lib/shopify_cli/command_options/command_serve_options.rb +10 -0
  45. data/lib/shopify_cli/commands/app/serve.rb +7 -7
  46. data/lib/shopify_cli/commands/login.rb +5 -2
  47. data/lib/shopify_cli/context.rb +13 -0
  48. data/lib/shopify_cli/git.rb +36 -0
  49. data/lib/shopify_cli/identity_auth.rb +24 -4
  50. data/lib/shopify_cli/messages/messages.rb +26 -5
  51. data/lib/shopify_cli/release.rb +194 -0
  52. data/lib/shopify_cli/sed.rb +19 -0
  53. data/lib/shopify_cli/services/app/create/rails_service.rb +10 -2
  54. data/lib/shopify_cli/services/app/serve/node_service.rb +2 -25
  55. data/lib/shopify_cli/services/app/serve/php_service.rb +2 -25
  56. data/lib/shopify_cli/services/app/serve/rails_service.rb +8 -28
  57. data/lib/shopify_cli/services/app/serve/serve_service.rb +57 -0
  58. data/lib/shopify_cli/services.rb +1 -0
  59. data/lib/shopify_cli/tasks/update_dashboard_urls.rb +7 -9
  60. data/lib/shopify_cli/theme/dev_server/hot-reload.js +40 -13
  61. data/lib/shopify_cli/theme/dev_server/hot_reload/remote_file_reloader.rb +1 -1
  62. data/lib/shopify_cli/theme/dev_server/hot_reload/sections_index.rb +51 -0
  63. data/lib/shopify_cli/theme/dev_server/hot_reload.rb +6 -1
  64. data/lib/shopify_cli/theme/dev_server/local_assets.rb +1 -1
  65. data/lib/shopify_cli/theme/dev_server/remote_watcher/json_files_update_job.rb +35 -0
  66. data/lib/shopify_cli/theme/dev_server/remote_watcher.rb +44 -0
  67. data/lib/shopify_cli/theme/dev_server/watcher.rb +2 -8
  68. data/lib/shopify_cli/theme/dev_server.rb +18 -5
  69. data/lib/shopify_cli/theme/file.rb +15 -4
  70. data/lib/shopify_cli/theme/syncer/checksums.rb +60 -0
  71. data/lib/shopify_cli/theme/syncer/forms/apply_to_all.rb +39 -0
  72. data/lib/shopify_cli/theme/syncer/forms/apply_to_all_form.rb +35 -0
  73. data/lib/shopify_cli/theme/syncer/forms/base_strategy_form.rb +62 -0
  74. data/lib/shopify_cli/theme/syncer/forms/select_delete_strategy.rb +27 -0
  75. data/lib/shopify_cli/theme/syncer/forms/select_update_strategy.rb +28 -0
  76. data/lib/shopify_cli/theme/syncer/ignore_helper.rb +33 -0
  77. data/lib/shopify_cli/theme/syncer/json_delete_handler.rb +51 -0
  78. data/lib/shopify_cli/theme/syncer/json_update_handler.rb +82 -0
  79. data/lib/shopify_cli/theme/syncer/merger.rb +53 -0
  80. data/lib/shopify_cli/theme/syncer/operation.rb +1 -1
  81. data/lib/shopify_cli/theme/syncer.rb +79 -63
  82. data/lib/shopify_cli/theme/theme.rb +26 -4
  83. data/lib/shopify_cli/theme/theme_admin_api.rb +23 -8
  84. data/lib/shopify_cli/thread_pool/job.rb +10 -2
  85. data/lib/shopify_cli/thread_pool.rb +15 -3
  86. data/lib/shopify_cli/tunnel.rb +9 -0
  87. data/lib/shopify_cli/version.rb +1 -1
  88. data/shopify-cli.gemspec +3 -1
  89. data/vendor/deps/cli-ui/lib/cli/ui/os.rb +8 -0
  90. metadata +30 -3
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shopify_cli/thread_pool"
4
+
5
+ require_relative "remote_watcher/json_files_update_job"
6
+
7
+ module ShopifyCLI
8
+ module Theme
9
+ module DevServer
10
+ class RemoteWatcher
11
+ SYNC_INTERVAL = 3 # seconds
12
+
13
+ class << self
14
+ def to(theme:, syncer:)
15
+ new(theme, syncer)
16
+ end
17
+ end
18
+
19
+ def start
20
+ thread_pool.schedule(recurring_job)
21
+ end
22
+
23
+ def stop
24
+ thread_pool.shutdown
25
+ end
26
+
27
+ private
28
+
29
+ def initialize(theme, syncer)
30
+ @theme = theme
31
+ @syncer = syncer
32
+ end
33
+
34
+ def thread_pool
35
+ @thread_pool ||= ShopifyCLI::ThreadPool.new(pool_size: 1)
36
+ end
37
+
38
+ def recurring_job
39
+ JsonFilesUpdateJob.new(@theme, @syncer, SYNC_INTERVAL)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -46,20 +46,14 @@ module ShopifyCLI
46
46
  files
47
47
  .select { |file| @theme.theme_file?(file) }
48
48
  .map { |file| @theme[file] }
49
- .reject { |file| ignore_file?(file) }
49
+ .reject { |file| @syncer.ignore_file?(file) }
50
50
  end
51
51
 
52
52
  def filter_remote_files(files)
53
53
  files
54
54
  .select { |file| @syncer.remote_file?(file) }
55
55
  .map { |file| @theme[file] }
56
- .reject { |file| ignore_file?(file) }
57
- end
58
-
59
- private
60
-
61
- def ignore_file?(file)
62
- @ignore_filter&.ignore?(file.relative_path.to_s)
56
+ .reject { |file| @syncer.ignore_file?(file) }
63
57
  end
64
58
  end
65
59
  end
@@ -11,6 +11,7 @@ require_relative "dev_server/local_assets"
11
11
  require_relative "dev_server/proxy"
12
12
  require_relative "dev_server/sse"
13
13
  require_relative "dev_server/watcher"
14
+ require_relative "dev_server/remote_watcher"
14
15
  require_relative "dev_server/web_server"
15
16
  require_relative "dev_server/certificate_manager"
16
17
 
@@ -26,12 +27,13 @@ module ShopifyCLI
26
27
  class << self
27
28
  attr_accessor :ctx
28
29
 
29
- def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false, mode: ReloadMode.default)
30
+ def start(ctx, root, host: "127.0.0.1", port: 9292, poll: false, editor_sync: false, mode: ReloadMode.default)
30
31
  @ctx = ctx
31
32
  theme = DevelopmentTheme.find_or_create!(ctx, root: root)
32
33
  ignore_filter = IgnoreFilter.from_path(root)
33
- @syncer = Syncer.new(ctx, theme: theme, ignore_filter: ignore_filter)
34
- watcher = Watcher.new(ctx, theme: theme, syncer: @syncer, ignore_filter: ignore_filter, poll: poll)
34
+ @syncer = Syncer.new(ctx, theme: theme, ignore_filter: ignore_filter, overwrite_json: !editor_sync)
35
+ watcher = Watcher.new(ctx, theme: theme, syncer: @syncer, poll: poll)
36
+ remote_watcher = RemoteWatcher.to(theme: theme, syncer: @syncer)
35
37
 
36
38
  # Setup the middleware stack. Mimics Rack::Builder / config.ru, but in reverse order
37
39
  @app = Proxy.new(ctx, theme: theme, syncer: @syncer)
@@ -57,9 +59,17 @@ module ShopifyCLI
57
59
 
58
60
  return if stopped
59
61
 
62
+ preview_suffix = editor_sync ? "" : ctx.message("theme.serve.download_changes")
63
+ preview_message = ctx.message(
64
+ "theme.serve.customize_or_preview",
65
+ preview_suffix,
66
+ theme.editor_url,
67
+ theme.preview_url
68
+ )
69
+
60
70
  ctx.puts(ctx.message("theme.serve.serving", theme.root))
61
71
  ctx.open_url!(address)
62
- ctx.puts(ctx.message("theme.serve.customize_or_preview", theme.editor_url, theme.preview_url))
72
+ ctx.puts(preview_message)
63
73
  end
64
74
 
65
75
  logger = if ctx.debug?
@@ -69,6 +79,7 @@ module ShopifyCLI
69
79
  end
70
80
 
71
81
  watcher.start
82
+ remote_watcher.start if editor_sync
72
83
  WebServer.run(
73
84
  @app,
74
85
  BindAddress: host,
@@ -76,11 +87,13 @@ module ShopifyCLI
76
87
  Logger: logger,
77
88
  AccessLog: [],
78
89
  )
90
+ remote_watcher.stop if editor_sync
79
91
  watcher.stop
80
92
 
81
93
  rescue ShopifyCLI::API::APIRequestForbiddenError,
82
94
  ShopifyCLI::API::APIRequestUnauthorizedError
83
- raise ShopifyCLI::Abort, @ctx.message("theme.serve.ensure_user", theme.shop)
95
+ shop = ShopifyCLI::AdminAPI.get_shop_or_abort(@ctx)
96
+ raise ShopifyCLI::Abort, @ctx.message("theme.serve.ensure_user", shop)
84
97
  rescue Errno::EADDRINUSE
85
98
  error_message = @ctx.message("theme.serve.address_already_in_use", address)
86
99
  help_message = @ctx.message("theme.serve.try_port_option")
@@ -4,7 +4,6 @@ require_relative "mime_type"
4
4
  module ShopifyCLI
5
5
  module Theme
6
6
  class File < Struct.new(:path)
7
- attr_reader :relative_path
8
7
  attr_accessor :remote_checksum
9
8
 
10
9
  def initialize(path, root)
@@ -42,7 +41,7 @@ module ShopifyCLI
42
41
  end
43
42
 
44
43
  def mime_type
45
- @mime_type ||= MimeType.by_filename(relative_path)
44
+ @mime_type ||= MimeType.by_filename(@relative_path)
46
45
  end
47
46
 
48
47
  def text?
@@ -54,7 +53,7 @@ module ShopifyCLI
54
53
  end
55
54
 
56
55
  def liquid_css?
57
- relative_path.to_s.end_with?(".css.liquid")
56
+ relative_path.end_with?(".css.liquid")
58
57
  end
59
58
 
60
59
  def json?
@@ -62,7 +61,7 @@ module ShopifyCLI
62
61
  end
63
62
 
64
63
  def template?
65
- relative_path.to_s.start_with?("templates/")
64
+ relative_path.start_with?("templates/")
66
65
  end
67
66
 
68
67
  def checksum
@@ -84,6 +83,18 @@ module ShopifyCLI
84
83
  relative_path == other.relative_path
85
84
  end
86
85
 
86
+ def name(*args)
87
+ ::File.basename(path, *args)
88
+ end
89
+
90
+ def absolute_path
91
+ path.realpath.to_s
92
+ end
93
+
94
+ def relative_path
95
+ @relative_path.to_s
96
+ end
97
+
87
98
  private
88
99
 
89
100
  def normalize_json(content)
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ class Checksums
7
+ def initialize(theme)
8
+ @theme = theme
9
+ @checksum_by_key = {}
10
+
11
+ # Mutex used to coordinate changes in the checksums (shared accross `Syncer` threads)
12
+ @checksums_mutex = Mutex.new
13
+ end
14
+
15
+ def has?(file)
16
+ checksum_by_key.key?(to_key(file))
17
+ end
18
+
19
+ def file_has_changed?(file)
20
+ file.checksum != checksum_by_key[file.relative_path]
21
+ end
22
+
23
+ def keys
24
+ checksum_by_key.keys
25
+ end
26
+
27
+ def [](key)
28
+ checksum_by_key[key]
29
+ end
30
+
31
+ def []=(key, value)
32
+ checksums_mutex.synchronize do
33
+ checksum_by_key[key] = value
34
+ end
35
+ end
36
+
37
+ # Generate .liquid asset files are reported twice in checksum:
38
+ # once of generated, once for .liquid. We only keep the .liquid, that's the one we have
39
+ # on disk.
40
+ def reject_duplicated_checksums!
41
+ checksums_mutex.synchronize do
42
+ checksum_by_key.reject! { |key, _| checksum_by_key.key?("#{key}.liquid") }
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def to_key(file)
49
+ theme[file].relative_path
50
+ end
51
+
52
+ # Private getters only used in unit tests
53
+
54
+ attr_reader :checksum_by_key
55
+ attr_reader :theme
56
+ attr_reader :checksums_mutex
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "apply_to_all_form"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ module Forms
9
+ class ApplyToAll
10
+ attr_reader :value
11
+
12
+ def initialize(ctx, number_of_files)
13
+ @ctx = ctx
14
+ @number_of_files = number_of_files
15
+ @value = nil
16
+ @apply = nil
17
+ end
18
+
19
+ def apply?(value)
20
+ return unless @number_of_files > 1
21
+
22
+ if @apply.nil?
23
+ @apply = ask.apply?
24
+ @value = value if @apply
25
+ end
26
+
27
+ @apply
28
+ end
29
+
30
+ private
31
+
32
+ def ask
33
+ ApplyToAllForm.ask(@ctx, [], number_of_files: @number_of_files)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ module Forms
7
+ class ApplyToAllForm < ShopifyCLI::Form
8
+ attr_accessor :apply
9
+ flag_arguments :number_of_files
10
+
11
+ def ask
12
+ title = message("title", number_of_files - 1)
13
+
14
+ self.apply = CLI::UI::Prompt.ask(title, allow_empty: false) do |handler|
15
+ handler.option(message("yes")) { true }
16
+ handler.option(message("no")) { false }
17
+ end
18
+
19
+ self
20
+ end
21
+
22
+ def apply?
23
+ apply
24
+ end
25
+
26
+ private
27
+
28
+ def message(key, *params)
29
+ ctx.message("theme.serve.syncer.forms.apply_to_all.#{key}", *params)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ module Forms
7
+ class BaseStrategyForm < ShopifyCLI::Form
8
+ attr_accessor :strategy
9
+
10
+ def ask
11
+ ctx.puts(title_context(file))
12
+
13
+ self.strategy = CLI::UI::Prompt.ask(title_question, allow_empty: false) do |handler|
14
+ strategies.each do |strategy|
15
+ handler.option(as_text(strategy)) { strategy }
16
+ end
17
+ end
18
+
19
+ exit_cli if self.strategy == :exit
20
+
21
+ self
22
+ end
23
+
24
+ protected
25
+
26
+ ##
27
+ # List of strategies that populate the form options
28
+ #
29
+ def strategies
30
+ raise "`#{self.class.name}#strategies' must be defined"
31
+ end
32
+
33
+ ##
34
+ # Message prefix for the form title and options (strategies).
35
+ # See the methods `title` and `as_text`
36
+ #
37
+ def prefix
38
+ raise "`#{self.class.name}#prefix' must be defined"
39
+ end
40
+
41
+ private
42
+
43
+ def exit_cli
44
+ exit(0)
45
+ end
46
+
47
+ def title_context(file)
48
+ ctx.message("#{prefix}.title_context", file.relative_path)
49
+ end
50
+
51
+ def title_question
52
+ ctx.message("#{prefix}.title_question")
53
+ end
54
+
55
+ def as_text(strategy)
56
+ ctx.message("#{prefix}.#{strategy}")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy_form"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ module Forms
9
+ class SelectDeleteStrategy < BaseStrategyForm
10
+ flag_arguments :file
11
+
12
+ def strategies
13
+ %i[
14
+ delete
15
+ restore
16
+ exit
17
+ ]
18
+ end
19
+
20
+ def prefix
21
+ "theme.serve.syncer.forms.delete_strategy"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_strategy_form"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ module Forms
9
+ class SelectUpdateStrategy < BaseStrategyForm
10
+ flag_arguments :file
11
+
12
+ def strategies
13
+ %i[
14
+ keep_remote
15
+ keep_local
16
+ union_merge
17
+ exit
18
+ ]
19
+ end
20
+
21
+ def prefix
22
+ "theme.serve.syncer.forms.update_strategy"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyCLI
4
+ module Theme
5
+ class Syncer
6
+ module IgnoreHelper
7
+ def ignore_operation?(operation)
8
+ path = operation.file_path
9
+ ignore_path?(path)
10
+ end
11
+
12
+ def ignore_file?(file)
13
+ path = file.path
14
+ ignore_path?(path)
15
+ end
16
+
17
+ def ignore_path?(path)
18
+ ignored_by_ignore_filter?(path) || ignored_by_include_filter?(path)
19
+ end
20
+
21
+ private
22
+
23
+ def ignored_by_ignore_filter?(path)
24
+ ignore_filter&.ignore?(path)
25
+ end
26
+
27
+ def ignored_by_include_filter?(path)
28
+ !!include_filter && !include_filter.match?(path)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "forms/apply_to_all"
4
+ require_relative "forms/select_delete_strategy"
5
+
6
+ module ShopifyCLI
7
+ module Theme
8
+ class Syncer
9
+ module JsonDeleteHandler
10
+ def enqueue_json_deletes(files)
11
+ files = files.select { |file| !ignore_file?(file) }
12
+
13
+ if overwrite_json?
14
+ enqueue_deletes(files)
15
+ else
16
+ # Handle conflicts when JSON files cannot be overwritten
17
+ handle_delete_conflicts(files)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def handle_delete_conflicts(files)
24
+ to_delete = []
25
+ to_get = []
26
+
27
+ apply_to_all = Forms::ApplyToAll.new(@ctx, files.size)
28
+
29
+ files.each do |file|
30
+ delete_strategy = apply_to_all.value || ask_delete_strategy(file)
31
+ apply_to_all.apply?(delete_strategy)
32
+
33
+ case delete_strategy
34
+ when :delete
35
+ to_delete << file
36
+ when :restore
37
+ to_get << file
38
+ end
39
+ end
40
+
41
+ enqueue_deletes(to_delete)
42
+ enqueue_get(to_get)
43
+ end
44
+
45
+ def ask_delete_strategy(file)
46
+ Forms::SelectDeleteStrategy.ask(@ctx, [], file: file).strategy
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "forms/apply_to_all"
4
+ require_relative "forms/select_update_strategy"
5
+
6
+ module ShopifyCLI
7
+ module Theme
8
+ class Syncer
9
+ module JsonUpdateHandler
10
+ def enqueue_json_updates(files)
11
+ # Some files must be uploaded after the other ones
12
+ delayed_files = [
13
+ theme["config/settings_schema.json"],
14
+ theme["config/settings_data.json"],
15
+ ]
16
+
17
+ # Update remote JSON files and delays `delayed_files` update
18
+ files = files
19
+ .select { |file| !ignore_file?(file) && file.exist? && checksums.file_has_changed?(file) }
20
+ .sort_by { |file| delayed_files.include?(file) ? 1 : 0 }
21
+
22
+ if overwrite_json?
23
+ enqueue_updates(files)
24
+ else
25
+ # Handle conflicts when JSON files cannot be overwritten
26
+ handle_update_conflicts(files)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def handle_update_conflicts(files)
33
+ to_get = []
34
+ to_delete = []
35
+ to_update = []
36
+ to_union_merge = []
37
+
38
+ apply_to_all = Forms::ApplyToAll.new(@ctx, files.size)
39
+
40
+ files.each do |file|
41
+ update_strategy = apply_to_all.value || ask_update_strategy(file)
42
+ apply_to_all.apply?(update_strategy)
43
+
44
+ case update_strategy
45
+ when :keep_remote
46
+ if file_exist_remotely?(file)
47
+ to_get << file
48
+ else
49
+ delete_locally(file)
50
+ end
51
+ when :keep_local
52
+ to_update << file
53
+ when :union_merge
54
+ if file_exist_remotely?(file)
55
+ to_union_merge << file
56
+ else
57
+ to_update << file
58
+ end
59
+ end
60
+ end
61
+
62
+ enqueue_get(to_get)
63
+ enqueue_deletes(to_delete)
64
+ enqueue_updates(to_update)
65
+ enqueue_union_merges(to_union_merge)
66
+ end
67
+
68
+ def file_exist_remotely?(file)
69
+ !checksums[file.relative_path].nil?
70
+ end
71
+
72
+ def delete_locally(file)
73
+ ::File.delete(file.absolute_path)
74
+ end
75
+
76
+ def ask_update_strategy(file)
77
+ Forms::SelectUpdateStrategy.ask(@ctx, [], file: file).strategy
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tempfile"
4
+
5
+ module ShopifyCLI
6
+ module Theme
7
+ class Syncer
8
+ class Merger
9
+ class << self
10
+ ##
11
+ # Merge `theme_file` with the `new_content` by relying on the union merge
12
+ #
13
+ def union_merge(theme_file, new_content)
14
+ git_merge(theme_file, new_content, ["--union", "-p"])
15
+ end
16
+
17
+ private
18
+
19
+ ##
20
+ # Merge theme file (`ShopifyCLI::Theme::File`) with a new content (String),
21
+ # by creating a temporary file based on the `new_content`.
22
+ #
23
+ def git_merge(theme_file, new_content, opts)
24
+ remote_file = create_tmp_file(tmp_file_name(theme_file), new_content)
25
+ empty_file = create_tmp_file("empty")
26
+
27
+ ShopifyCLI::Git.merge_file(
28
+ theme_file.absolute_path,
29
+ empty_file.path,
30
+ remote_file.path,
31
+ opts
32
+ )
33
+ ensure
34
+ # Remove temporary files on Windows as well
35
+ remote_file.close!
36
+ empty_file.close!
37
+ end
38
+
39
+ def create_tmp_file(basename, content = "")
40
+ tmp_file = Tempfile.new(basename)
41
+ tmp_file.write(content)
42
+ tmp_file.close # Make it ready to merge
43
+ tmp_file
44
+ end
45
+
46
+ def tmp_file_name(ref_file)
47
+ "shopify-cli-merge-#{ref_file.name(".*")}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -35,7 +35,7 @@ module ShopifyCLI
35
35
  end
36
36
 
37
37
  def file_path
38
- file&.relative_path.to_s
38
+ file&.relative_path
39
39
  end
40
40
 
41
41
  private