1# frozen_string_literal: true
2
3module Gitlab
4  class PushOptions
5    VALID_OPTIONS = HashWithIndifferentAccess.new({
6      merge_request: {
7        keys: [
8          :assign,
9          :create,
10          :description,
11          :label,
12          :merge_when_pipeline_succeeds,
13          :milestone,
14          :remove_source_branch,
15          :target,
16          :title,
17          :unassign,
18          :unlabel
19        ]
20      },
21      ci: {
22        keys: [:skip, :variable]
23      }
24    }).freeze
25
26    MULTI_VALUE_OPTIONS = [
27      %w[ci variable],
28      %w[merge_request label],
29      %w[merge_request unlabel],
30      %w[merge_request assign],
31      %w[merge_request unassign]
32    ].freeze
33
34    NAMESPACE_ALIASES = HashWithIndifferentAccess.new({
35      mr: :merge_request
36    }).freeze
37
38    OPTION_MATCHER = /(?<namespace>[^\.]+)\.(?<key>[^=]+)=?(?<value>.*)/.freeze
39
40    CI_SKIP = 'ci.skip'
41
42    attr_reader :options
43
44    def initialize(options = [])
45      @options = parse_options(options)
46    end
47
48    def get(*args)
49      options.dig(*args)
50    end
51
52    # Allow #to_json serialization
53    def as_json(*_args)
54      options
55    end
56
57    private
58
59    def parse_options(raw_options)
60      options = HashWithIndifferentAccess.new
61
62      Array.wrap(raw_options).each do |option|
63        namespace, key, value = parse_option(option)
64
65        next if [namespace, key].any?(&:nil?)
66
67        store_option_info(options, namespace, key, value)
68      end
69
70      options
71    end
72
73    def store_option_info(options, namespace, key, value)
74      options[namespace] ||= HashWithIndifferentAccess.new
75
76      if option_multi_value?(namespace, key)
77        options[namespace][key] ||= HashWithIndifferentAccess.new(0)
78        options[namespace][key][value] += 1
79      else
80        options[namespace][key] = value
81      end
82    end
83
84    def option_multi_value?(namespace, key)
85      MULTI_VALUE_OPTIONS.any? { |arr| arr == [namespace, key] }
86    end
87
88    def parse_option(option)
89      parts = OPTION_MATCHER.match(option)
90      return unless parts
91
92      namespace, key, value = parts.values_at(:namespace, :key, :value).map(&:strip)
93      namespace = NAMESPACE_ALIASES[namespace] if NAMESPACE_ALIASES[namespace]
94      value = value.presence || true
95
96      return unless valid_option?(namespace, key)
97
98      [namespace, key, value]
99    end
100
101    def valid_option?(namespace, key)
102      keys = VALID_OPTIONS.dig(namespace, :keys)
103      keys && keys.include?(key.to_sym)
104    end
105  end
106end
107