gitlab-cloud-connector 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -3
  3. data/CONTRIBUTING.md +50 -0
  4. data/LICENSE +28 -0
  5. data/Makefile +33 -0
  6. data/README.md +11 -221
  7. data/Rakefile +1 -5
  8. data/config/add_ons/duo_enterprise.yml +2 -0
  9. data/config/add_ons/duo_pro.yml +2 -0
  10. data/config/backend_services/ai_gateway.yml +5 -0
  11. data/config/backend_services/duo_workflow_service.yml +5 -0
  12. data/config/backend_services/observability.yml +5 -0
  13. data/config/backend_services/security_gateway.yml +5 -0
  14. data/config/license_types/premium.yml +2 -0
  15. data/config/license_types/ultimate.yml +2 -0
  16. data/config/schemas/add_on_schema.json +15 -0
  17. data/config/schemas/backend_service_schema.json +26 -0
  18. data/config/schemas/license_type_schema.json +15 -0
  19. data/config/schemas/unit_primitive_schema.json +97 -0
  20. data/config/unit_primitives/ask_build.yml +15 -0
  21. data/config/unit_primitives/ask_commit.yml +15 -0
  22. data/config/unit_primitives/ask_epic.yml +15 -0
  23. data/config/unit_primitives/ask_issue.yml +15 -0
  24. data/config/unit_primitives/code_suggestions.yml +19 -0
  25. data/config/unit_primitives/complete_code.yml +19 -0
  26. data/config/unit_primitives/documentation_search.yml +19 -0
  27. data/config/unit_primitives/duo_chat.yml +19 -0
  28. data/config/unit_primitives/duo_workflow_execute_workflow.yml +13 -0
  29. data/config/unit_primitives/explain_code.yml +17 -0
  30. data/config/unit_primitives/explain_vulnerability.yml +17 -0
  31. data/config/unit_primitives/fix_code.yml +17 -0
  32. data/config/unit_primitives/generate_code.yml +19 -0
  33. data/config/unit_primitives/generate_commit_message.yml +17 -0
  34. data/config/unit_primitives/generate_issue_description.yml +15 -0
  35. data/config/unit_primitives/glab_ask_git_command.yml +17 -0
  36. data/config/unit_primitives/include_dependency_context.yml +15 -0
  37. data/config/unit_primitives/include_file_context.yml +17 -0
  38. data/config/unit_primitives/include_issue_context.yml +15 -0
  39. data/config/unit_primitives/include_merge_request_context.yml +15 -0
  40. data/config/unit_primitives/include_snippet_context.yml +17 -0
  41. data/config/unit_primitives/observability_all.yml +14 -0
  42. data/config/unit_primitives/refactor_code.yml +17 -0
  43. data/config/unit_primitives/resolve_vulnerability.yml +17 -0
  44. data/config/unit_primitives/security_scans.yml +13 -0
  45. data/config/unit_primitives/summarize_comments.yml +17 -0
  46. data/config/unit_primitives/summarize_review.yml +15 -0
  47. data/config/unit_primitives/troubleshoot_job.yml +17 -0
  48. data/config/unit_primitives/write_tests.yml +17 -0
  49. data/lib/cloud_connector.rb +10 -0
  50. data/lib/gitlab/cloud_connector/json_web_token.rb +59 -0
  51. data/lib/gitlab/cloud_connector/{backend_service.rb → version.rb} +1 -3
  52. metadata +85 -48
  53. data/.idea/.gitignore +0 -8
  54. data/.idea/gitlab-cloud-connector.iml +0 -127
  55. data/.idea/inspectionProfiles/Project_Default.xml +0 -9
  56. data/.idea/misc.xml +0 -4
  57. data/.idea/modules.xml +0 -8
  58. data/.idea/vcs.xml +0 -7
  59. data/CHANGELOG.md +0 -5
  60. data/Dangerfile +0 -5
  61. data/config/cloud_connector/backend_services/ai_gateway/unit_primitives/code_suggestions.yml +0 -19
  62. data/config/cloud_connector/backend_services/ai_gateway/unit_primitives/documentation_search.yml +0 -19
  63. data/config/cloud_connector/backend_services/ai_gateway/unit_primitives/duo_chat.yml +0 -19
  64. data/lib/gitlab/cloud_connector/auth/json_web_token.rb +0 -82
  65. data/lib/gitlab/cloud_connector/base_group.rb +0 -39
  66. data/lib/gitlab/cloud_connector/configuration.rb +0 -24
  67. data/lib/gitlab/cloud_connector/deliverable.rb +0 -31
  68. data/lib/gitlab/cloud_connector/delivered_by.rb +0 -10
  69. data/lib/gitlab/cloud_connector/purchasable_decorator.rb +0 -59
  70. data/lib/gitlab/cloud_connector/token_issuer.rb +0 -44
  71. data/lib/gitlab/cloud_connector/unit_primitives.rb +0 -195
  72. data/lib/gitlab/cloud_connector.rb +0 -68
  73. data/lib/gitlab/popen.rb +0 -68
  74. data/lib/utils/parse_helper.rb +0 -15
  75. data/lib/utils/strong_memoize.rb +0 -145
  76. data/sig/gitlab/cloud_connector.rbs +0 -0
@@ -1,59 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../../utils/strong_memoize'
4
- require_relative '../../utils/parse_helper'
5
- require "active_support"
6
- require "active_support/core_ext/module/delegation"
7
-
8
- module Gitlab
9
- module CloudConnector
10
- class PurchasableDecorator
11
- include ::Utils::StrongMemoize
12
- include ::Utils::ParseHelper
13
-
14
- delegate_missing_to :unit_primitive_or_group
15
-
16
- def initialize(unit_primitive_or_group)
17
- @unit_primitive_or_group = unit_primitive_or_group
18
- end
19
-
20
- def available_for?(_resource)
21
- raise 'Not Implemented'
22
- end
23
-
24
- def free_access?
25
- !cut_off_date&.past?
26
- end
27
-
28
- def available?(resource, gitlab_version, gitlab_realm)
29
- return false unless gitlab_realms.include?(gitlab_realm)
30
-
31
- supported_gitlab_version?(gitlab_version) && (free_access? || available_for?(resource))
32
- end
33
-
34
- def supported_gitlab_version?(gitlab_version)
35
- min_gitlab_version = calculate_min_gitlab_version
36
-
37
- return true unless min_gitlab_version
38
- return false unless gitlab_version
39
- # Extract only major and minor version (ignore -ee, -pre and patch suffixes)
40
- gitlab_version = parse_version(gitlab_version)
41
-
42
- gitlab_version >= min_gitlab_version
43
- end
44
-
45
- def calculate_min_gitlab_version
46
- if free_access?
47
- min_gitlab_version_for_free_access
48
- else
49
- min_gitlab_version
50
- end
51
- end
52
-
53
- private
54
-
55
- attr_reader :unit_primitive_or_group
56
-
57
- end
58
- end
59
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'auth/json_web_token'
4
-
5
- module Gitlab
6
- module CloudConnector
7
- ###
8
- # Provides access information for GitLab services offered under Cloud Connector.
9
- #
10
- # Self-Managed GitLab instances sync with this service to get access to Cloud Connector
11
- # features based on billing status and other information.
12
- module TokenIssuer
13
-
14
- attr_reader :subject, :extra_claims
15
-
16
- def access_token(resource, gitlab_version: nil, gitlab_realm: nil, subject: nil, extra_claims: {})
17
- subject ||= ::Gitlab::CloudConnector.token_subject
18
- gitlab_version ||= ::Gitlab::CloudConnector.gitlab_version
19
- gitlab_realm ||= ::Gitlab::CloudConnector.gitlab_realm
20
-
21
- allowed_scopes = allowed_scopes(resource, gitlab_version, gitlab_realm)
22
-
23
- return nil if allowed_scopes.empty?
24
-
25
- ::Gitlab::CloudConnector::Auth::JsonWebToken.new(
26
- audience: allowed_backends,
27
- subject: subject,
28
- scopes: allowed_scopes,
29
- extra_claims: extra_claims
30
- )
31
- end
32
-
33
- private
34
-
35
- def allowed_backends
36
- unit_primitives.values.map{ |unit_primitive| "gitlab-#{unit_primitive.backend_service.dasherize}"}.uniq
37
- end
38
-
39
- def allowed_scopes(resource, gitlab_version, gitlab_realm)
40
- unit_primitives.values.select{ |unit_primitive| unit_primitive.available?(resource, gitlab_version, gitlab_realm) }.map(&:name)
41
- end
42
- end
43
- end
44
- end
@@ -1,195 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../../utils/parse_helper'
4
-
5
- module Gitlab
6
- module CloudConnector
7
- class UnitPrimitives
8
- include ::Utils::ParseHelper
9
-
10
- attr_reader :path
11
- attr_reader :attributes
12
-
13
- VALID_UNIT_PRIMITIVE_NAME = %r{^#{/[a-z0-9]+/}(#{'_'} #{/[a-z0-9]+/})*$}x
14
- BACKEND_SERVICE_MATCHER = %r{\/backend_services\/(\w+)\/unit_primitives\/(?:\w+)\.yml}
15
-
16
- PARAMS = %i[
17
- name
18
- description
19
- backend_service
20
- cut_off_date
21
- introduced_by_url
22
- unit_primitive_issue_url
23
- milestone
24
- group
25
- feature_category
26
- maturity
27
- min_gitlab_version
28
- min_gitlab_version_for_free_access
29
- min_gitlab_version
30
- min_backend_version
31
- delivered_by
32
- bundled_with
33
- gitlab_realms
34
- ].freeze
35
-
36
- BACKEND_SERVICES = %i[
37
- ai_gateway
38
- ]
39
-
40
- InvalidUnitPrimitiveError = Class.new(Exception)
41
-
42
- PARAMS.each do |param|
43
- define_method(param) do
44
- ## parse time or create version
45
- attributes[param]
46
- end
47
- end
48
-
49
- BACKEND_SERVICES.each do |backend_service, _|
50
- define_method("#{backend_service}?") do
51
- attributes[:backend_service].to_sym == backend_service
52
- end
53
- end
54
-
55
- alias :add_ons :bundled_with
56
-
57
- def initialize(path, **opts)
58
- @path = path
59
- @attributes = {}
60
-
61
- PARAMS.each do |param|
62
- if param.end_with?('version')
63
- @attributes[param] = parse_version(opts[param]) if opts[param]
64
- elsif param.end_with?('date')
65
- @attributes[param] = parse_time(opts[param]).utc if opts[param]
66
- else
67
- @attributes[param] = opts[param]
68
- end
69
- end
70
-
71
- # read backend_service from the path
72
- @attributes[:backend_service] = BACKEND_SERVICE_MATCHER.match(path.to_s)[1]
73
- end
74
-
75
- def key
76
- name.to_sym
77
- end
78
-
79
- def validate!
80
- unless name.present?
81
- raise InvalidUnitPrimitiveError, "Unit primitive is missing name"
82
- end
83
-
84
- if backend_service.empty?
85
- raise InvalidUnitPrimitiveError, "Unit primitive '#{name}' is missing `backend_service`. Ensure to update #{path}"
86
- end
87
-
88
- unless BACKEND_SERVICES.include?(backend_service.to_sym)
89
- raise InvalidUnitPrimitiveError, "Unit primitive '#{name}' backend service '#{backend_service}' is invalid."
90
- end
91
-
92
- if delivered_by.empty?
93
- raise InvalidUnitPrimitiveError, "Unit primitive '#{name}' is missing 'delivered_by'. If unit primitive is delivered as a stand-alone feature, set unit_primitive name."
94
- end
95
-
96
- if bundled_with.empty?
97
- raise InvalidUnitPrimitiveError, "Unit primitive '#{name}' is missing 'bundled_with'."
98
- end
99
-
100
- if feature_category.empty?
101
- raise InvalidUnitPrimitiveError, "Unit primitive '#{name}' is missing 'feature_category'."
102
- end
103
-
104
- unless VALID_UNIT_PRIMITIVE_NAME =~ name
105
- raise InvalidUnitPrimitiveError, "Unit primitive '#{name}' is invalid"
106
- end
107
-
108
- if File.basename(path, ".yml") != name
109
- raise InvalidUnitPrimitiveError, "Unit primitive '#{name}' has an invalid path: '#{path}'. Ensure to update #{path}"
110
- end
111
- end
112
-
113
- def to_h
114
- attributes
115
- end
116
-
117
- class << self
118
- def paths
119
- @paths ||= [File.join(CloudConnector.root, 'config', 'cloud_connector', 'backend_services', '**', 'unit_primitives', '*.yml')]
120
- end
121
-
122
- def definitions
123
- # We lazily load all definitions
124
- @definitions ||= load_all!
125
- end
126
-
127
- def get(key)
128
- definitions[key.to_sym]
129
- end
130
-
131
- def reload!
132
- @definitions = load_all!
133
- end
134
-
135
- def has_definition?(key)
136
- definitions.has_key?(key.to_sym)
137
- end
138
-
139
- def register_hot_reloader!
140
- return unless Rails.env.development? || Rails.env.test?
141
- # Reload feature flags on change of this file or any `.yml`
142
- file_watcher = Rails.configuration.file_watcher.new(reload_files, reload_directories) do
143
- reload!
144
- end
145
-
146
- Rails.application.reloaders << file_watcher
147
- Rails.application.reloader.to_run { file_watcher.execute_if_updated }
148
-
149
- file_watcher
150
- end
151
-
152
- def load_all!
153
- paths.each_with_object({}) do |glob_path, definitions|
154
- load_all_from_path!(definitions, glob_path)
155
- end
156
- end
157
-
158
- def load_from_file(path)
159
- definition = File.read(path)
160
- definition = YAML.safe_load(definition, aliases: true)
161
- definition.deep_symbolize_keys!
162
-
163
- self.new(path, **definition).tap(&:validate!)
164
- rescue StandardError => e
165
- raise InvalidUnitPrimitiveError, "Invalid definition for `#{path}`: #{e.message}"
166
- end
167
-
168
- def load_all_from_path!(definitions, glob_path)
169
- Dir.glob(glob_path).each do |path|
170
- definition = load_from_file(path)
171
-
172
- if previous = definitions[definition.key]
173
- raise InvalidUnitPrimitiveError, "Feature flag '#{definition.key}' is already defined in '#{previous.path}'"
174
- end
175
-
176
- definitions[definition.key] = definition
177
- end
178
- end
179
-
180
- def reload_files
181
- []
182
- end
183
-
184
- def reload_directories
185
- paths.each_with_object({}) do |path, result|
186
- path = File.dirname(path)
187
- Dir.glob(path).each do |matching_dir|
188
- result[matching_dir] = 'yml'
189
- end
190
- end
191
- end
192
- end
193
- end
194
- end
195
- end
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'cloud_connector/configuration'
4
- require_relative 'cloud_connector/token_issuer'
5
- require_relative 'cloud_connector/deliverable'
6
- require_relative 'cloud_connector/delivered_by'
7
- require_relative 'cloud_connector/backend_service'
8
- require_relative '../utils/strong_memoize'
9
-
10
- module Gitlab
11
- module CloudConnector
12
- extend Deliverable
13
- extend TokenIssuer
14
- extend ::Utils::StrongMemoize
15
- Time.zone = 'UTC'
16
-
17
- VERSION = "0.1.0"
18
-
19
- class << self
20
-
21
- alias :instance_access_token :access_token
22
- delegate :token_issuer, :token_expires_in, :token_subject, :gitlab_realm, :gitlab_version, to: :configuration
23
-
24
- def root
25
- File.expand_path '../../..', __FILE__
26
- end
27
-
28
- def configure
29
- yield configuration
30
- end
31
-
32
- def configuration
33
- strong_memoize(:configuration) do
34
- Configuration.new
35
- end
36
- end
37
-
38
- def unit_primitives
39
- strong_memoize(:unit_primitives) do
40
- unit_primitives = Gitlab::CloudConnector::UnitPrimitives.definitions
41
- unit_primitives.transform_values{ |up| configuration.purchasable_decorator_class.new(up) }
42
- end
43
- end
44
-
45
- def backend_services
46
- strong_memoize(:backend_services) do
47
- unit_primitives.values.group_by(&:backend_service).map do |name, unit_primitives|
48
- BackendService.new(name.to_sym, unit_primitives.index_by(&:key))
49
- end.index_by(&:name)
50
- end
51
- end
52
-
53
- def feature_categories
54
- strong_memoize(:feature_categories) do
55
- unit_primitives.values.map(&:feature_category).uniq
56
- end
57
- end
58
-
59
- def bundled_with
60
- strong_memoize(:bundled_with) do
61
- unit_primitives.values.flat_map(&:bundled_with).uniq
62
- end
63
- end
64
-
65
- alias :add_ons :bundled_with
66
- end
67
- end
68
- end
data/lib/gitlab/popen.rb DELETED
@@ -1,68 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
- require 'open3'
5
-
6
- module Gitlab
7
- module Popen
8
- extend self
9
-
10
- Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)
11
-
12
- # Returns [stdout + stderr, status]
13
- # status is either the exit code or the signal that killed the process
14
- def popen(cmd, path = nil, vars = {}, &block)
15
- result = popen_with_detail(cmd, path, vars, &block)
16
-
17
- # Process#waitpid returns Process::Status, which holds a 16-bit value.
18
- # The higher-order 8 bits hold the exit() code (`exitstatus`).
19
- # The lower-order bits holds whether the process was terminated.
20
- # If the process didn't exit normally, `exitstatus` will be `nil`,
21
- # but we still want a non-zero code, even if the value is
22
- # platform-dependent.
23
- status = result.status&.exitstatus || result.status.to_i
24
-
25
- ["#{result.stdout}#{result.stderr}", status]
26
- end
27
-
28
- # Returns Result
29
- def popen_with_detail(cmd, path = nil, vars = {})
30
- unless cmd.is_a?(Array)
31
- raise "System commands must be given as an array of strings"
32
- end
33
-
34
- if cmd.one? && cmd.first.match?(/\s/)
35
- raise "System commands must be split into an array of space-separated values"
36
- end
37
-
38
- path ||= Dir.pwd
39
- vars['PWD'] = path
40
- options = { chdir: path }
41
-
42
- unless File.directory?(path)
43
- FileUtils.mkdir_p(path)
44
- end
45
-
46
- cmd_stdout = ''
47
- cmd_stderr = ''
48
- cmd_status = nil
49
- start = Time.now
50
-
51
- Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
52
- # stderr and stdout pipes can block if stderr/stdout aren't drained: https://bugs.ruby-lang.org/issues/9082
53
- # Mimic what Ruby does with capture3: https://github.com/ruby/ruby/blob/1ec544695fa02d714180ef9c34e755027b6a2103/lib/open3.rb#L257-L273
54
- out_reader = Thread.new { stdout.read }
55
- err_reader = Thread.new { stderr.read }
56
-
57
- yield(stdin) if block_given?
58
- stdin.close
59
-
60
- cmd_stdout = out_reader.value
61
- cmd_stderr = err_reader.value
62
- cmd_status = wait_thr.value
63
- end
64
-
65
- Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start)
66
- end
67
- end
68
- end
@@ -1,15 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Utils
4
- module ParseHelper
5
- VERSION_REGEX = /\A(\d+\.\d+)/
6
- def parse_time(time)
7
- Time.zone.parse(time).utc if time
8
- end
9
-
10
- def parse_version(version)
11
- version = VERSION_REGEX.match(version.to_s).to_s
12
- Gem::Version.new(version)
13
- end
14
- end
15
- end
@@ -1,145 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Utils
4
- module StrongMemoize
5
- # Instead of writing patterns like this:
6
- #
7
- # def trigger_from_token
8
- # return @trigger if defined?(@trigger)
9
- #
10
- # @trigger = Ci::Trigger.find_by_token(params[:token].to_s)
11
- # end
12
- #
13
- # We could write it like:
14
- #
15
- # include Utils::StrongMemoize
16
- #
17
- # def trigger_from_token
18
- # Ci::Trigger.find_by_token(params[:token].to_s)
19
- # end
20
- # strong_memoize_attr :trigger_from_token
21
- #
22
- # def enabled?
23
- # Feature.enabled?(:some_feature)
24
- # end
25
- # strong_memoize_attr :enabled?
26
- #
27
- def strong_memoize(name)
28
- key = ivar(name)
29
-
30
- if instance_variable_defined?(key)
31
- instance_variable_get(key)
32
- else
33
- instance_variable_set(key, yield)
34
- end
35
- end
36
-
37
- # Works the same way as "strong_memoize" but takes
38
- # a second argument - expire_in. This allows invalidate
39
- # the data after specified number of seconds
40
- def strong_memoize_with_expiration(name, expire_in)
41
- key = ivar(name)
42
- expiration_key = "#{key}_expired_at"
43
-
44
- if instance_variable_defined?(expiration_key)
45
- expire_at = instance_variable_get(expiration_key)
46
- clear_memoization(name) if Time.current > expire_at
47
- end
48
-
49
- if instance_variable_defined?(key)
50
- instance_variable_get(key)
51
- else
52
- value = instance_variable_set(key, yield)
53
- instance_variable_set(expiration_key, Time.current + expire_in)
54
- value
55
- end
56
- end
57
-
58
- def strong_memoize_with(name, *args)
59
- container = strong_memoize(name) { {} }
60
-
61
- if container.key?(args)
62
- container[args]
63
- else
64
- container[args] = yield
65
- end
66
- end
67
-
68
- def strong_memoized?(name)
69
- key = ivar(StrongMemoize.normalize_key(name))
70
- instance_variable_defined?(key)
71
- end
72
-
73
- def clear_memoization(name)
74
- key = ivar(StrongMemoize.normalize_key(name))
75
- remove_instance_variable(key) if instance_variable_defined?(key)
76
- end
77
-
78
- module StrongMemoizeClassMethods
79
- def strong_memoize_attr(method_name)
80
- member_name = StrongMemoize.normalize_key(method_name)
81
-
82
- StrongMemoize.send(:do_strong_memoize, self, method_name, member_name) # rubocop:disable GitlabSecurity/PublicSend
83
- end
84
- end
85
-
86
- def self.included(base)
87
- base.singleton_class.prepend(StrongMemoizeClassMethods)
88
- end
89
-
90
- private
91
-
92
- # Convert `"name"`/`:name` into `:@name`
93
- #
94
- # Depending on a type ensure that there's a single memory allocation
95
- def ivar(name)
96
- case name
97
- when Symbol
98
- name.to_s.prepend("@").to_sym
99
- when String
100
- :"@#{name}"
101
- else
102
- raise ArgumentError, "Invalid type of '#{name}'"
103
- end
104
- end
105
-
106
- class << self
107
- def normalize_key(key)
108
- return key unless key.end_with?('!', '?')
109
-
110
- # Replace invalid chars like `!` and `?` with allowed Unicode codeparts.
111
- key.to_s.tr('!?', "\uFF01\uFF1F")
112
- end
113
-
114
- private
115
-
116
- def do_strong_memoize(klass, method_name, member_name)
117
- method = klass.instance_method(method_name)
118
-
119
- unless method.arity == 0
120
- raise <<~ERROR
121
- Using `strong_memoize_attr` on methods with parameters is not supported.
122
-
123
- Use `strong_memoize_with` instead.
124
- See https://docs.gitlab.com/ee/development/utilities.html#strongmemoize
125
- ERROR
126
- end
127
-
128
- # Methods defined within a class method are already public by default, so we don't need to
129
- # explicitly make them public.
130
- scope = %i[private protected].find do |scope|
131
- klass.send(:"#{scope}_instance_methods") # rubocop:disable GitlabSecurity/PublicSend
132
- .include? method_name
133
- end
134
-
135
- klass.define_method(method_name) do |&block|
136
- strong_memoize(member_name) do
137
- method.bind_call(self, &block)
138
- end
139
- end
140
-
141
- klass.send(scope, method_name) if scope # rubocop:disable GitlabSecurity/PublicSend
142
- end
143
- end
144
- end
145
- end
File without changes