Make PushOptions a cross-domain framework
## Problem
We are using push options as Hash object in many parts of the application and this has turned into a [Primitive Obsession](https://wiki.c2.com/?PrimitiveObsession). Push options is more of a framework for passing data to GitLab backend via `git push`. Today we have various types of data (namespaces) of push options such as `ci`, `merge_request`, `integrations`, etc.
While ~"group::source code" would own the framework (of passing data), each data namespace should be owned by various domains. The proposal below can be one way we could solve this in a modularized way.
## Context
This issue is extracted from a code review where we introduced a CI-specific push options class rather than having specific parser logic defined in `Git::BaseHooksService`.
The following discussion from !186297 should be addressed:
- [ ] @fabiopitino started a [discussion](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186297#note_2438200807):
> **non-blocking**: I just realized that we do have `Gitlab::PushOptions` object that that is used in `PostReceiveService` https://gitlab.com/gitlab-org/gitlab/-/blob/c5c3ed711c5f47a4d872ecd63ad86451b09be56e/app/services/post_receive_service.rb#L61 but later it's pushed to the `PostReceive` Sidekiq worker [as JSON](https://gitlab.com/gitlab-org/gitlab/-/blob/c5c3ed711c5f47a4d872ecd63ad86451b09be56e/app/services/post_receive_service.rb#L33) and from there on we use it as Hash.
>
> It would be great to have a SSoT for dealing with push options but it would require an even bigger refactoring because:
> * `Gitlab::PushOptions` expects options in Array form (as it comes from Gitaly).
> * The initializer of `Gitlab::PushOptions` is primarily a parser. The rest of the class is a basic access object.
> * We could have `Gitlab::PushOptions` to be a SSoT for serialized and deserialized options
> * Each domain could implement their own Push Options namespace (e.g. `ci.*`) and be responsible for it.
> * The ~"group::source code" would be responsible for the general push options framework while each team owns their own namespace
>
> ```ruby
> class Gitlab::PushOptions
> def self.fabricate(push_options)
> # handle: array (gitaly), hash/json, PushOptions object, nil
> end
>
> NAMESPACES = {
> ci: ::Ci::PushOptions, # each domain owns their own push options
> merge_request: ::MergeRequests::PushOptions,
> # ...
> }
>
> def initialize(data)
> @data = data
> @opts = {}
>
> NAMESPACES.each do |namespace, handler|
> @opts[namespace] = handler.new(data.fetch(namespace.to_sym, {}))
> end
>
> @opts.each(&:validate!)
> end
>
> def ci
> @opts.fetch(:ci)
> end
>
> def merge_request
> @opts.fetch(:merge_request)
> end
>
> def as_json(*_args)
> @data.as_json
> end
> end
>
> class Gitlab::PushOptions::Namespace
> def initialize(data)
> @data = data
> end
>
> def validate!
> # use locally defined ALLOWED_OPTIONS and MULTI_VALUE_OPTIONS
> end
> end
>
> class Ci::PushOptions < Gitlab::PushOptions::Namespace
> ALLOWED_OPTIONS = [:skip, :variable, :input]
> MULTI_VALUE_OPTIONS = [:input, :variable]
>
> def skip?
> data[:skip].present?
> end
>
> def variables
> # ...
> end
>
> def inputs
> # ...
> end
> end
>
> ####
> # Usage:
> push_options = Gitlab::PushOptions.fabricate(data)
> push_options.ci.skip? # => false
> push_options.ci.inputs # => { foo: 'bar' }
> push_options.merge_request.labels # => ['foo', 'bar']
> push_options.secret_push_protection.skip_all? # => true
> push_options.as_json # => { ... }
> ```
issue