Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
173bd0618f
commit
4e8387dc14
33 changed files with 507 additions and 115 deletions
|
@ -549,33 +549,36 @@ rspec-ee unit pg11 geo:
|
|||
- .rails:rules:ee-only-unit
|
||||
- .rspec-ee-unit-geo-parallel
|
||||
|
||||
rspec-ee unit pg11 geo minimal:
|
||||
extends:
|
||||
- rspec-ee unit pg11 geo
|
||||
- .minimal-rspec-tests
|
||||
- .rails:rules:ee-only-unit:minimal
|
||||
# FIXME: Temporarily disable geo minimal rspec jobs https://gitlab.com/gitlab-org/gitlab/-/issues/294212
|
||||
#rspec-ee unit pg11 geo minimal:
|
||||
# extends:
|
||||
# - rspec-ee unit pg11 geo
|
||||
# - .minimal-rspec-tests
|
||||
# - .rails:rules:ee-only-unit:minimal
|
||||
|
||||
rspec-ee integration pg11 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg11
|
||||
- .rails:rules:ee-only-integration
|
||||
|
||||
rspec-ee integration pg11 geo minimal:
|
||||
extends:
|
||||
- rspec-ee integration pg11 geo
|
||||
- .minimal-rspec-tests
|
||||
- .rails:rules:ee-only-integration:minimal
|
||||
# FIXME: Temporarily disable geo minimal rspec jobs https://gitlab.com/gitlab-org/gitlab/-/issues/294212
|
||||
#rspec-ee integration pg11 geo minimal:
|
||||
# extends:
|
||||
# - rspec-ee integration pg11 geo
|
||||
# - .minimal-rspec-tests
|
||||
# - .rails:rules:ee-only-integration:minimal
|
||||
|
||||
rspec-ee system pg11 geo:
|
||||
extends:
|
||||
- .rspec-ee-base-geo-pg11
|
||||
- .rails:rules:ee-only-system
|
||||
|
||||
rspec-ee system pg11 geo minimal:
|
||||
extends:
|
||||
- rspec-ee system pg11 geo
|
||||
- .minimal-rspec-tests
|
||||
- .rails:rules:ee-only-system:minimal
|
||||
# FIXME: Temporarily disable geo minimal rspec jobs https://gitlab.com/gitlab-org/gitlab/-/issues/294212
|
||||
#rspec-ee system pg11 geo minimal:
|
||||
# extends:
|
||||
# - rspec-ee system pg11 geo
|
||||
# - .minimal-rspec-tests
|
||||
# - .rails:rules:ee-only-system:minimal
|
||||
|
||||
db:rollback geo:
|
||||
extends:
|
||||
|
|
|
@ -10,3 +10,12 @@ export const CONTENT_UPDATE_DEBOUNCE = 250;
|
|||
export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
|
||||
'Editor Lite instance is required to set up an extension.',
|
||||
);
|
||||
|
||||
//
|
||||
// EXTENSIONS' CONSTANTS
|
||||
//
|
||||
|
||||
// For CI config schemas the filename must match
|
||||
// '*.gitlab-ci.yml' regardless of project configuration.
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
|
||||
export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml';
|
||||
|
|
34
app/assets/javascripts/editor/editor_ci_schema_ext.js
Normal file
34
app/assets/javascripts/editor/editor_ci_schema_ext.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import Api from '~/api';
|
||||
import { registerSchema } from '~/ide/utils';
|
||||
import { EditorLiteExtension } from './editor_lite_extension_base';
|
||||
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from './constants';
|
||||
|
||||
export class CiSchemaExtension extends EditorLiteExtension {
|
||||
/**
|
||||
* Registers a syntax schema to the editor based on project
|
||||
* identifier and commit.
|
||||
*
|
||||
* The schema is added to the file that is currently edited
|
||||
* in the editor.
|
||||
*
|
||||
* @param {Object} opts
|
||||
* @param {String} opts.projectNamespace
|
||||
* @param {String} opts.projectPath
|
||||
* @param {String?} opts.ref - Current ref. Defaults to master
|
||||
*/
|
||||
registerCiSchema({ projectNamespace, projectPath, ref = 'master' } = {}) {
|
||||
const ciSchemaUri = Api.buildUrl(Api.projectFileSchemaPath)
|
||||
.replace(':namespace_path', projectNamespace)
|
||||
.replace(':project_path', projectPath)
|
||||
.replace(':ref', ref)
|
||||
.replace(':filename', EXTENSION_CI_SCHEMA_FILE_NAME_MATCH);
|
||||
const modelFileName = this.getModel()
|
||||
.uri.path.split('/')
|
||||
.pop();
|
||||
|
||||
registerSchema({
|
||||
uri: ciSchemaUri,
|
||||
fileMatch: [modelFileName],
|
||||
});
|
||||
}
|
||||
}
|
|
@ -832,21 +832,21 @@ UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
|
|||
};
|
||||
|
||||
UsersSelect.prototype.renderApprovalRules = function(elsClassName, approvalRules = []) {
|
||||
if (!gon.features?.reviewerApprovalRules || !elsClassName?.includes('reviewer')) {
|
||||
const count = approvalRules.length;
|
||||
|
||||
if (!gon.features?.reviewerApprovalRules || !elsClassName?.includes('reviewer') || !count) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const count = approvalRules.length;
|
||||
const [rule] = approvalRules;
|
||||
const countText = sprintf(__('(+%{count} rules)'), { count });
|
||||
const renderApprovalRulesCount = count > 1 ? `<span class="ml-1">${countText}</span>` : '';
|
||||
const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : rule.name;
|
||||
|
||||
return count
|
||||
? `<div class="gl-display-flex gl-font-sm">
|
||||
<span class="gl-text-truncate" title="${rule.name}">${rule.name}</span>
|
||||
${renderApprovalRulesCount}
|
||||
</div>`
|
||||
: '';
|
||||
return `<div class="gl-display-flex gl-font-sm">
|
||||
<span class="gl-text-truncate" title="${ruleName}">${ruleName}</span>
|
||||
${renderApprovalRulesCount}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
export default UsersSelect;
|
||||
|
|
|
@ -84,6 +84,9 @@ export default {
|
|||
onFileChange() {
|
||||
this.$emit('input', this.editor.getValue());
|
||||
},
|
||||
getEditor() {
|
||||
return this.editor;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
GfmAutocompleteType,
|
||||
tributeConfig,
|
||||
} from 'ee_else_ce/vue_shared/components/gfm_autocomplete/utils';
|
||||
import * as Emoji from '~/emoji';
|
||||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
|
@ -76,6 +77,14 @@ export default {
|
|||
return (inputText, processValues) => {
|
||||
if (this.cache[type]) {
|
||||
processValues(this.filterValues(type));
|
||||
} else if (type === GfmAutocompleteType.Emojis) {
|
||||
Emoji.initEmojiMap()
|
||||
.then(() => {
|
||||
const emojis = Emoji.getValidEmojiNames();
|
||||
this.cache[type] = emojis;
|
||||
processValues(emojis);
|
||||
})
|
||||
.catch(() => createFlash({ message: this.$options.errorMessage }));
|
||||
} else if (this.dataSources[type]) {
|
||||
axios
|
||||
.get(this.dataSources[type])
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { escape, last } from 'lodash';
|
||||
import * as Emoji from '~/emoji';
|
||||
import { spriteIcon } from '~/lib/utils/common_utils';
|
||||
|
||||
const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
|
||||
|
@ -6,6 +7,7 @@ const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings
|
|||
const nonWordOrInteger = /\W|^\d+$/;
|
||||
|
||||
export const GfmAutocompleteType = {
|
||||
Emojis: 'emojis',
|
||||
Issues: 'issues',
|
||||
Labels: 'labels',
|
||||
Members: 'members',
|
||||
|
@ -21,6 +23,15 @@ function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
|
|||
}
|
||||
|
||||
export const tributeConfig = {
|
||||
[GfmAutocompleteType.Emojis]: {
|
||||
config: {
|
||||
trigger: ':',
|
||||
lookup: value => value,
|
||||
menuItemTemplate: ({ original }) => `${original} ${Emoji.glEmojiTag(original)}`,
|
||||
selectTemplate: ({ original }) => `:${original}:`,
|
||||
},
|
||||
},
|
||||
|
||||
[GfmAutocompleteType.Issues]: {
|
||||
config: {
|
||||
trigger: '#',
|
||||
|
|
|
@ -169,7 +169,7 @@ export default {
|
|||
return new GLForm(
|
||||
$(this.$refs['gl-form']),
|
||||
{
|
||||
emojis: this.enableAutocomplete,
|
||||
emojis: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
|
||||
members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
|
||||
issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
|
||||
mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
* MR link https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22716
|
||||
*/
|
||||
.gl-select2-html5-required-fix div.select2-container+select.select2 {
|
||||
@include gl-opacity-0;
|
||||
@include gl-border-0;
|
||||
@include gl-bg-none;
|
||||
@include gl-bg-transparent;
|
||||
display: block !important;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
margin: -3px auto 0;
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
|
|
5
changelogs/unreleased/keep_n_int.yml
Normal file
5
changelogs/unreleased/keep_n_int.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Ensure container_expiration_policy keep_n is an integer
|
||||
merge_request: 49805
|
||||
author: Mathieu Parent
|
||||
type: changed
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: null_hypothesis
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45840
|
||||
rollout_issue_url:
|
||||
type: experiment
|
||||
group: group::adoption
|
||||
default_enabled: false
|
|
@ -1057,7 +1057,7 @@ POST /projects
|
|||
| `build_timeout` | integer | **{dotted-circle}** No | The maximum amount of time in minutes that a job is able run (in seconds). |
|
||||
| `builds_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
|
||||
| `ci_config_path` | string | **{dotted-circle}** No | The path to CI configuration file. |
|
||||
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
|
||||
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
|
||||
| `container_registry_enabled` | boolean | **{dotted-circle}** No | Enable container registry for this project. |
|
||||
| `default_branch` | string | **{dotted-circle}** No | `master` by default. |
|
||||
| `description` | string | **{dotted-circle}** No | Short project description. |
|
||||
|
@ -1206,7 +1206,7 @@ PUT /projects/:id
|
|||
| `ci_config_path` | string | **{dotted-circle}** No | The path to CI configuration file. |
|
||||
| `ci_default_git_depth` | integer | **{dotted-circle}** No | Default number of revisions for [shallow cloning](../ci/pipelines/settings.md#git-shallow-clone). |
|
||||
| `ci_forward_deployment_enabled` | boolean | **{dotted-circle}** No | When a new deployment job starts, [skip older deployment jobs](../ci/pipelines/settings.md#skip-outdated-deployment-jobs) that are still pending |
|
||||
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
|
||||
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
|
||||
| `container_registry_enabled` | boolean | **{dotted-circle}** No | Enable container registry for this project. |
|
||||
| `default_branch` | string | **{dotted-circle}** No | `master` by default. |
|
||||
| `description` | string | **{dotted-circle}** No | Short project description. |
|
||||
|
|
|
@ -65,6 +65,14 @@ users have to fix their `.gitlab-ci.yml` that could annoy their workflow.
|
|||
|
||||
Please read [versioning](#versioning) section for introducing breaking change safely.
|
||||
|
||||
### Best practices
|
||||
|
||||
- Avoid using [global keywords](../../ci/yaml/README.md#global-keywords),
|
||||
such as `image`, `stages` and `variables` at top-level.
|
||||
When a root `.gitlab-ci.yml` [includes](../../ci/yaml/README.md#include)
|
||||
multiple templates, these global keywords could be overridden by the
|
||||
others and cause an unexpected behavior.
|
||||
|
||||
## Versioning
|
||||
|
||||
Versioning allows you to introduce a new template without modifying the existing
|
||||
|
|
|
@ -20,6 +20,9 @@ IP rate limits**:
|
|||
|
||||
These limits are disabled by default.
|
||||
|
||||
NOTE:
|
||||
By default, all Git operations are first tried unathenticated. Because of this, HTTP Git operations may trigger the rate limits configured for unauthenticated requests.
|
||||
|
||||
![user-and-ip-rate-limits](img/user_and_ip_rate_limits.png)
|
||||
|
||||
## Use an HTTP header to bypass rate limiting
|
||||
|
|
|
@ -236,18 +236,24 @@ lock your files to prevent any conflicting changes.
|
|||
|
||||
You can access your repositories via [repository API](../../../api/repositories.md).
|
||||
|
||||
## Clone in Apple Xcode
|
||||
## Clone a repository
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/45820) in GitLab 11.0
|
||||
Learn how to [clone a repository through the command line](../../../gitlab-basics/start-using-git.md#clone-a-repository).
|
||||
|
||||
Alternatively, clone directly into a code editor as documented below.
|
||||
|
||||
### Clone to Apple Xcode
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/45820) in GitLab 11.0.
|
||||
|
||||
Projects that contain a `.xcodeproj` or `.xcworkspace` directory can now be cloned
|
||||
in Xcode using the new **Open in Xcode** button, located next to the Git URL
|
||||
into Xcode using the new **Open in Xcode** button, located next to the Git URL
|
||||
used for cloning your project. The button is only shown on macOS.
|
||||
|
||||
## Download Source Code
|
||||
|
||||
> Support for directory download was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/24704) in GitLab 11.11.
|
||||
> Support for [including Git LFS blobs](../../../topics/git/lfs#lfs-objects-in-project-archives) was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15079) in GitLab 13.5.
|
||||
> - Support for directory download was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/24704) in GitLab 11.11.
|
||||
> - Support for [including Git LFS blobs](../../../topics/git/lfs#lfs-objects-in-project-archives) was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15079) in GitLab 13.5.
|
||||
|
||||
The source code stored in a repository can be downloaded from the UI.
|
||||
By clicking the download icon, a dropdown will open with links to download the following:
|
||||
|
|
13
lib/api/entities/basic_repository_storage_move.rb
Normal file
13
lib/api/entities/basic_repository_storage_move.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class BasicRepositoryStorageMove < Grape::Entity
|
||||
expose :id
|
||||
expose :created_at
|
||||
expose :human_state_name, as: :state
|
||||
expose :source_storage_name
|
||||
expose :destination_storage_name
|
||||
end
|
||||
end
|
||||
end
|
18
lib/api/entities/basic_snippet.rb
Normal file
18
lib/api/entities/basic_snippet.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class BasicSnippet < Grape::Entity
|
||||
expose :id, :title, :description, :visibility
|
||||
expose :updated_at, :created_at
|
||||
expose :project_id
|
||||
expose :web_url do |snippet|
|
||||
Gitlab::UrlBuilder.build(snippet)
|
||||
end
|
||||
expose :raw_url do |snippet|
|
||||
Gitlab::UrlBuilder.build(snippet, raw: true)
|
||||
end
|
||||
expose :ssh_url_to_repo, :http_url_to_repo, if: ->(snippet) { snippet.repository_exists? }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,12 +2,7 @@
|
|||
|
||||
module API
|
||||
module Entities
|
||||
class ProjectRepositoryStorageMove < Grape::Entity
|
||||
expose :id
|
||||
expose :created_at
|
||||
expose :human_state_name, as: :state
|
||||
expose :source_storage_name
|
||||
expose :destination_storage_name
|
||||
class ProjectRepositoryStorageMove < BasicRepositoryStorageMove
|
||||
expose :project, using: Entities::ProjectIdentity
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,18 +2,8 @@
|
|||
|
||||
module API
|
||||
module Entities
|
||||
class Snippet < Grape::Entity
|
||||
expose :id, :title, :description, :visibility
|
||||
class Snippet < BasicSnippet
|
||||
expose :author, using: Entities::UserBasic
|
||||
expose :updated_at, :created_at
|
||||
expose :project_id
|
||||
expose :web_url do |snippet|
|
||||
Gitlab::UrlBuilder.build(snippet)
|
||||
end
|
||||
expose :raw_url do |snippet|
|
||||
Gitlab::UrlBuilder.build(snippet, raw: true)
|
||||
end
|
||||
expose :ssh_url_to_repo, :http_url_to_repo, if: ->(snippet) { snippet.repository_exists? }
|
||||
expose :file_name do |snippet|
|
||||
snippet.file_name_on_repo || snippet.file_name
|
||||
end
|
||||
|
|
9
lib/api/entities/snippet_repository_storage_move.rb
Normal file
9
lib/api/entities/snippet_repository_storage_move.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
class SnippetRepositoryStorageMove < BasicRepositoryStorageMove
|
||||
expose :snippet, using: Entities::BasicSnippet
|
||||
end
|
||||
end
|
||||
end
|
|
@ -99,7 +99,7 @@ module API
|
|||
|
||||
params :optional_container_expiration_policy_params do
|
||||
optional :cadence, type: String, desc: 'Container expiration policy cadence for recurring job'
|
||||
optional :keep_n, type: String, desc: 'Container expiration policy number of images to keep'
|
||||
optional :keep_n, type: Integer, desc: 'Container expiration policy number of images to keep'
|
||||
optional :older_than, type: String, desc: 'Container expiration policy remove images older than value'
|
||||
optional :name_regex, type: String, desc: 'Container expiration policy regex for image removal'
|
||||
optional :name_regex_keep, type: String, desc: 'Container expiration policy regex for image retention'
|
||||
|
|
|
@ -57,6 +57,8 @@ class Feature
|
|||
default_enabled: false,
|
||||
example: <<-EOS
|
||||
experiment(:my_experiment, project: project, actor: current_user) { ...variant code... }
|
||||
# or
|
||||
Gitlab::Experimentation.in_experiment_group?(:my_experiment, subject: current_user)
|
||||
EOS
|
||||
}
|
||||
}.freeze
|
||||
|
|
|
@ -87,6 +87,9 @@ module Gitlab
|
|||
},
|
||||
invite_members_empty_project_version_a: {
|
||||
tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyProjectVersionA'
|
||||
},
|
||||
trial_during_signup: {
|
||||
tracking_category: 'Growth::Conversion::Experiment::TrialDuringSignup'
|
||||
}
|
||||
}.freeze
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
module Gitlab
|
||||
module Experimentation
|
||||
class Experiment
|
||||
FEATURE_FLAG_SUFFIX = "_experiment_percentage"
|
||||
|
||||
attr_reader :key, :tracking_category, :use_backwards_compatible_subject_index
|
||||
|
||||
def initialize(key, **params)
|
||||
|
@ -10,7 +12,7 @@ module Gitlab
|
|||
@tracking_category = params[:tracking_category]
|
||||
@use_backwards_compatible_subject_index = params[:use_backwards_compatible_subject_index]
|
||||
|
||||
@experiment_percentage = Feature.get(:"#{key}_experiment_percentage").percentage_of_time_value # rubocop:disable Gitlab/AvoidFeatureGet
|
||||
@experiment_percentage = Feature.get(:"#{key}#{FEATURE_FLAG_SUFFIX}").percentage_of_time_value # rubocop:disable Gitlab/AvoidFeatureGet
|
||||
end
|
||||
|
||||
def active?
|
||||
|
|
|
@ -2820,9 +2820,6 @@ msgstr ""
|
|||
msgid "All changes are committed"
|
||||
msgstr ""
|
||||
|
||||
msgid "All default stages are currently visible"
|
||||
msgstr ""
|
||||
|
||||
msgid "All email addresses will be used to identify your commits."
|
||||
msgstr ""
|
||||
|
||||
|
@ -6910,6 +6907,9 @@ msgstr ""
|
|||
msgid "Code Coverage|Couldn't fetch the code coverage data"
|
||||
msgstr ""
|
||||
|
||||
msgid "Code Owner"
|
||||
msgstr ""
|
||||
|
||||
msgid "Code Owners"
|
||||
msgstr ""
|
||||
|
||||
|
@ -7329,6 +7329,9 @@ msgstr ""
|
|||
msgid "Congratulations! You have enabled Two-factor Authentication!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Congratulations, your free trial is activated."
|
||||
msgstr ""
|
||||
|
||||
msgid "Connect"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8330,9 +8333,21 @@ msgstr ""
|
|||
msgid "CustomCycleAnalytics|Add stage"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|All default stages are currently visible"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Default stages"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Editing stage"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|End event"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|End event label"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Enter a name for the stage"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8345,10 +8360,13 @@ msgstr ""
|
|||
msgid "CustomCycleAnalytics|Please select a start event first"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Select start event"
|
||||
msgid "CustomCycleAnalytics|Recover hidden stage"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Select stop event"
|
||||
msgid "CustomCycleAnalytics|Select end event"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Select start event"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Stage name already exists"
|
||||
|
@ -8357,18 +8375,12 @@ msgstr ""
|
|||
msgid "CustomCycleAnalytics|Start event"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Start event changed, please select a valid stop event"
|
||||
msgid "CustomCycleAnalytics|Start event changed, please select a valid end event"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Start event label"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Stop event"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Stop event label"
|
||||
msgstr ""
|
||||
|
||||
msgid "CustomCycleAnalytics|Update stage"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8959,9 +8971,6 @@ msgstr ""
|
|||
msgid "Default projects limit"
|
||||
msgstr ""
|
||||
|
||||
msgid "Default stages"
|
||||
msgstr ""
|
||||
|
||||
msgid "Default: Map a FogBugz account ID to a full name"
|
||||
msgstr ""
|
||||
|
||||
|
@ -12977,6 +12986,9 @@ msgstr ""
|
|||
msgid "GitLab Billing Team."
|
||||
msgstr ""
|
||||
|
||||
msgid "GitLab Gold trial (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "GitLab Group Runners can execute code for all the projects in this group."
|
||||
msgstr ""
|
||||
|
||||
|
@ -14223,6 +14235,9 @@ msgstr ""
|
|||
msgid "How many days need to pass between marking entity for deletion and actual removing it."
|
||||
msgstr ""
|
||||
|
||||
msgid "How many employees will use Gitlab?"
|
||||
msgstr ""
|
||||
|
||||
msgid "How many replicas each Elasticsearch shard has."
|
||||
msgstr ""
|
||||
|
||||
|
@ -29419,6 +29434,12 @@ msgstr ""
|
|||
msgid "Trials|You won't get a free trial right now but you can always resume this process by clicking on your avatar and choosing 'Start a free trial'"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Dismiss"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trial|Successful trial activation image"
|
||||
msgstr ""
|
||||
|
||||
msgid "Trigger"
|
||||
msgstr ""
|
||||
|
||||
|
@ -31055,6 +31076,9 @@ msgstr ""
|
|||
msgid "We want to be sure it is you, please confirm you are not a robot."
|
||||
msgstr ""
|
||||
|
||||
msgid "We will activate your trial on your group once you complete this step. After 30 days, you can upgrade to any plan"
|
||||
msgstr ""
|
||||
|
||||
msgid "We will notify %{inviter} that you declined their invitation to join GitLab. You will stop receiving reminders."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -426,36 +426,16 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
visit project_issue_path(project, issue)
|
||||
note = find('#note-body')
|
||||
|
||||
start_comment_with_emoji(note)
|
||||
start_comment_with_emoji(note, '.atwho-view li')
|
||||
|
||||
start_and_cancel_discussion
|
||||
|
||||
note.fill_in(with: '')
|
||||
start_comment_with_emoji(note)
|
||||
start_comment_with_emoji(note, '.atwho-view li')
|
||||
note.native.send_keys(:enter)
|
||||
|
||||
expect(note.value).to eql('Hello :100: ')
|
||||
end
|
||||
|
||||
def start_comment_with_emoji(note)
|
||||
note.native.send_keys('Hello :10')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find('.atwho-view li', text: '100')
|
||||
end
|
||||
|
||||
def start_and_cancel_discussion
|
||||
click_button('Reply...')
|
||||
|
||||
fill_in('note_note', with: 'Whoops!')
|
||||
|
||||
page.accept_alert 'Are you sure you want to cancel creating this comment?' do
|
||||
click_button('Cancel')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'autocomplete suggestions' do
|
||||
|
@ -599,6 +579,33 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
expect(page).not_to have_selector('.tribute-container', visible: true)
|
||||
end
|
||||
|
||||
it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do
|
||||
note = find('#note-body')
|
||||
|
||||
# Number.
|
||||
page.within '.timeline-content-form' do
|
||||
note.native.send_keys('7:')
|
||||
end
|
||||
|
||||
expect(page).not_to have_selector('.tribute-container', visible: true)
|
||||
|
||||
# ASCII letter.
|
||||
page.within '.timeline-content-form' do
|
||||
note.set('')
|
||||
note.native.send_keys('w:')
|
||||
end
|
||||
|
||||
expect(page).not_to have_selector('.tribute-container', visible: true)
|
||||
|
||||
# Non-ASCII letter.
|
||||
page.within '.timeline-content-form' do
|
||||
note.set('')
|
||||
note.native.send_keys('Ё:')
|
||||
end
|
||||
|
||||
expect(page).not_to have_selector('.tribute-container', visible: true)
|
||||
end
|
||||
|
||||
it 'selects the first item for assignee dropdowns' do
|
||||
page.within '.timeline-content-form' do
|
||||
find('#note-body').native.send_keys('@')
|
||||
|
@ -624,6 +631,16 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
expect(find('.tribute-container ul', visible: true)).to have_content(user.name)
|
||||
end
|
||||
|
||||
it 'selects the first item for non-assignee dropdowns if a query is entered' do
|
||||
page.within '.timeline-content-form' do
|
||||
find('#note-body').native.send_keys(':1')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find('.tribute-container ul', visible: true)).to have_selector('.highlight:first-of-type')
|
||||
end
|
||||
|
||||
context 'when autocompleting for groups' do
|
||||
it 'shows the group when searching for the name of the group' do
|
||||
page.within '.timeline-content-form' do
|
||||
|
@ -687,6 +704,25 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
expect_to_wrap(false, user_item, note, user.username)
|
||||
end
|
||||
|
||||
it 'does not wrap for emoji values' do
|
||||
note = find('#note-body')
|
||||
page.within '.timeline-content-form' do
|
||||
note.native.send_keys(":cartwheel_")
|
||||
end
|
||||
|
||||
emoji_item = first('.tribute-container li', text: 'cartwheel_tone1', visible: true)
|
||||
|
||||
expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
|
||||
end
|
||||
|
||||
it 'does not open autocomplete if there is no space before' do
|
||||
page.within '.timeline-content-form' do
|
||||
find('#note-body').native.send_keys("hello:#{user.username[0..2]}")
|
||||
end
|
||||
|
||||
expect(page).not_to have_selector('.tribute-container')
|
||||
end
|
||||
|
||||
it 'triggers autocomplete after selecting a quick action' do
|
||||
note = find('#note-body')
|
||||
page.within '.timeline-content-form' do
|
||||
|
@ -824,6 +860,26 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when other notes are destroyed' do
|
||||
let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
|
||||
|
||||
# This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
|
||||
it 'keeps autocomplete key listeners' do
|
||||
visit project_issue_path(project, issue)
|
||||
note = find('#note-body')
|
||||
|
||||
start_comment_with_emoji(note, '.tribute-container li')
|
||||
|
||||
start_and_cancel_discussion
|
||||
|
||||
note.fill_in(with: '')
|
||||
start_comment_with_emoji(note, '.tribute-container li')
|
||||
note.native.send_keys(:enter)
|
||||
|
||||
expect(note.value).to eql('Hello :100: ')
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'autocomplete suggestions' do
|
||||
it 'suggests objects correctly' do
|
||||
page.within '.timeline-content-form' do
|
||||
|
@ -913,4 +969,24 @@ RSpec.describe 'GFM autocomplete', :js do
|
|||
note.native.send_keys(text)
|
||||
end
|
||||
end
|
||||
|
||||
def start_comment_with_emoji(note, selector)
|
||||
note.native.send_keys('Hello :10')
|
||||
|
||||
wait_for_requests
|
||||
|
||||
find(selector, text: '100')
|
||||
end
|
||||
|
||||
def start_and_cancel_discussion
|
||||
click_button('Reply...')
|
||||
|
||||
fill_in('note_note', with: 'Whoops!')
|
||||
|
||||
page.accept_alert 'Are you sure you want to cancel creating this comment?' do
|
||||
click_button('Cancel')
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
end
|
||||
|
|
96
spec/frontend/editor/editor_ci_schema_ext_spec.js
Normal file
96
spec/frontend/editor/editor_ci_schema_ext_spec.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { languages } from 'monaco-editor';
|
||||
import EditorLite from '~/editor/editor_lite';
|
||||
import { CiSchemaExtension } from '~/editor/editor_ci_schema_ext';
|
||||
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '~/editor/constants';
|
||||
|
||||
describe('~/editor/editor_ci_config_ext', () => {
|
||||
const defaultBlobPath = '.gitlab-ci.yml';
|
||||
|
||||
let editor;
|
||||
let instance;
|
||||
let editorEl;
|
||||
|
||||
const createMockEditor = ({ blobPath = defaultBlobPath } = {}) => {
|
||||
setFixtures('<div id="editor"></div>');
|
||||
editorEl = document.getElementById('editor');
|
||||
editor = new EditorLite();
|
||||
instance = editor.createInstance({
|
||||
el: editorEl,
|
||||
blobPath,
|
||||
blobContent: '',
|
||||
});
|
||||
instance.use(new CiSchemaExtension());
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createMockEditor();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
instance.dispose();
|
||||
editorEl.remove();
|
||||
});
|
||||
|
||||
describe('registerCiSchema', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions');
|
||||
});
|
||||
|
||||
describe('register validations options with monaco for yaml language', () => {
|
||||
const mockProjectNamespace = 'namespace1';
|
||||
const mockProjectPath = 'project1';
|
||||
|
||||
const getConfiguredYmlSchema = () => {
|
||||
return languages.yaml.yamlDefaults.setDiagnosticsOptions.mock.calls[0][0].schemas[0];
|
||||
};
|
||||
|
||||
it('with expected basic validation configuration', () => {
|
||||
instance.registerCiSchema({
|
||||
projectNamespace: mockProjectNamespace,
|
||||
projectPath: mockProjectPath,
|
||||
});
|
||||
|
||||
const expectedOptions = {
|
||||
validate: true,
|
||||
enableSchemaRequest: true,
|
||||
hover: true,
|
||||
completion: true,
|
||||
};
|
||||
|
||||
expect(languages.yaml.yamlDefaults.setDiagnosticsOptions).toHaveBeenCalledTimes(1);
|
||||
expect(languages.yaml.yamlDefaults.setDiagnosticsOptions).toHaveBeenCalledWith(
|
||||
expect.objectContaining(expectedOptions),
|
||||
);
|
||||
});
|
||||
|
||||
it('with an schema uri that contains project and ref', () => {
|
||||
const mockRef = 'AABBCCDD';
|
||||
|
||||
instance.registerCiSchema({
|
||||
projectNamespace: mockProjectNamespace,
|
||||
projectPath: mockProjectPath,
|
||||
ref: mockRef,
|
||||
});
|
||||
|
||||
expect(getConfiguredYmlSchema()).toEqual({
|
||||
uri: `/${mockProjectNamespace}/${mockProjectPath}/-/schema/${mockRef}/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
|
||||
fileMatch: [defaultBlobPath],
|
||||
});
|
||||
});
|
||||
|
||||
it('with an alternative file name match', () => {
|
||||
createMockEditor({ blobPath: 'dir1/dir2/another-ci-filename.yml' });
|
||||
|
||||
instance.registerCiSchema({
|
||||
projectNamespace: mockProjectNamespace,
|
||||
projectPath: mockProjectPath,
|
||||
});
|
||||
|
||||
expect(getConfiguredYmlSchema()).toEqual({
|
||||
uri: `/${mockProjectNamespace}/${mockProjectPath}/-/schema/master/${EXTENSION_CI_SCHEMA_FILE_NAME_MATCH}`,
|
||||
fileMatch: ['another-ci-filename.yml'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -7,20 +7,22 @@ jest.mock('~/editor/editor_lite');
|
|||
|
||||
describe('Editor Lite component', () => {
|
||||
let wrapper;
|
||||
const onDidChangeModelContent = jest.fn();
|
||||
const updateModelLanguage = jest.fn();
|
||||
const getValue = jest.fn();
|
||||
const setValue = jest.fn();
|
||||
let mockInstance;
|
||||
|
||||
const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
|
||||
const fileName = 'lorem.txt';
|
||||
const fileGlobalId = 'snippet_777';
|
||||
const createInstanceMock = jest.fn().mockImplementation(() => ({
|
||||
onDidChangeModelContent,
|
||||
updateModelLanguage,
|
||||
getValue,
|
||||
setValue,
|
||||
dispose: jest.fn(),
|
||||
}));
|
||||
const createInstanceMock = jest.fn().mockImplementation(() => {
|
||||
mockInstance = {
|
||||
onDidChangeModelContent: jest.fn(),
|
||||
updateModelLanguage: jest.fn(),
|
||||
getValue: jest.fn(),
|
||||
setValue: jest.fn(),
|
||||
dispose: jest.fn(),
|
||||
};
|
||||
return mockInstance;
|
||||
});
|
||||
|
||||
Editor.mockImplementation(() => {
|
||||
return {
|
||||
createInstance: createInstanceMock,
|
||||
|
@ -46,8 +48,8 @@ describe('Editor Lite component', () => {
|
|||
});
|
||||
|
||||
const triggerChangeContent = val => {
|
||||
getValue.mockReturnValue(val);
|
||||
const [cb] = onDidChangeModelContent.mock.calls[0];
|
||||
mockInstance.getValue.mockReturnValue(val);
|
||||
const [cb] = mockInstance.onDidChangeModelContent.mock.calls[0];
|
||||
|
||||
cb();
|
||||
|
||||
|
@ -92,12 +94,12 @@ describe('Editor Lite component', () => {
|
|||
});
|
||||
|
||||
return nextTick().then(() => {
|
||||
expect(updateModelLanguage).toHaveBeenCalledWith(newFileName);
|
||||
expect(mockInstance.updateModelLanguage).toHaveBeenCalledWith(newFileName);
|
||||
});
|
||||
});
|
||||
|
||||
it('registers callback with editor onChangeContent', () => {
|
||||
expect(onDidChangeModelContent).toHaveBeenCalledWith(expect.any(Function));
|
||||
expect(mockInstance.onDidChangeModelContent).toHaveBeenCalledWith(expect.any(Function));
|
||||
});
|
||||
|
||||
it('emits input event when the blob content is changed', () => {
|
||||
|
@ -117,6 +119,10 @@ describe('Editor Lite component', () => {
|
|||
expect(wrapper.emitted()['editor-ready']).toBeDefined();
|
||||
});
|
||||
|
||||
it('component API `getEditor()` returns the editor instance', () => {
|
||||
expect(wrapper.vm.getEditor()).toBe(mockInstance);
|
||||
});
|
||||
|
||||
describe('reaction to the value update', () => {
|
||||
it('reacts to the changes in the passed value', async () => {
|
||||
const newValue = 'New Value';
|
||||
|
@ -126,7 +132,7 @@ describe('Editor Lite component', () => {
|
|||
});
|
||||
|
||||
await nextTick();
|
||||
expect(setValue).toHaveBeenCalledWith(newValue);
|
||||
expect(mockInstance.setValue).toHaveBeenCalledWith(newValue);
|
||||
});
|
||||
|
||||
it("does not update value if the passed one is exactly the same as the editor's content", async () => {
|
||||
|
@ -137,7 +143,7 @@ describe('Editor Lite component', () => {
|
|||
});
|
||||
|
||||
await nextTick();
|
||||
expect(setValue).not.toHaveBeenCalled();
|
||||
expect(mockInstance.setValue).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`gfm_autocomplete/utils emojis config shows the emoji name and icon in the menu item 1`] = `
|
||||
"raised_hands
|
||||
<gl-emoji
|
||||
|
||||
data-name=\\"raised_hands\\"></gl-emoji>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`gfm_autocomplete/utils issues config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context issue title <script>alert('hi')</script>"`;
|
||||
|
||||
exports[`gfm_autocomplete/utils issues config shows the reference and title in the menu item within a group context 1`] = `"<small>gitlab#987654</small> Group context issue title <script>alert('hi')</script>"`;
|
||||
|
|
|
@ -2,6 +2,27 @@ import { escape, last } from 'lodash';
|
|||
import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils';
|
||||
|
||||
describe('gfm_autocomplete/utils', () => {
|
||||
describe('emojis config', () => {
|
||||
const emojisConfig = tributeConfig[GfmAutocompleteType.Emojis].config;
|
||||
const emoji = 'raised_hands';
|
||||
|
||||
it('uses : as the trigger', () => {
|
||||
expect(emojisConfig.trigger).toBe(':');
|
||||
});
|
||||
|
||||
it('searches using the emoji name', () => {
|
||||
expect(emojisConfig.lookup(emoji)).toBe(emoji);
|
||||
});
|
||||
|
||||
it('shows the emoji name and icon in the menu item', () => {
|
||||
expect(emojisConfig.menuItemTemplate({ original: emoji })).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('inserts the emoji name on autocomplete selection', () => {
|
||||
expect(emojisConfig.selectTemplate({ original: emoji })).toBe(`:${emoji}:`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('issues config', () => {
|
||||
const issuesConfig = tributeConfig[GfmAutocompleteType.Issues].config;
|
||||
const groupContextIssue = {
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::Entities::SnippetRepositoryStorageMove do
|
||||
describe '#as_json' do
|
||||
subject { entity.as_json }
|
||||
|
||||
let(:default_storage) { 'default' }
|
||||
let(:second_storage) { 'test_second_storage' }
|
||||
let(:storage_move) { create(:snippet_repository_storage_move, :scheduled, destination_storage_name: second_storage) }
|
||||
let(:entity) { described_class.new(storage_move) }
|
||||
|
||||
it 'includes basic fields' do
|
||||
allow(Gitlab.config.repositories.storages).to receive(:keys).and_return(%W[#{default_storage} #{second_storage}])
|
||||
|
||||
is_expected.to include(
|
||||
state: 'scheduled',
|
||||
source_storage_name: default_storage,
|
||||
destination_storage_name: second_storage,
|
||||
snippet: a_kind_of(Hash)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2711,6 +2711,22 @@ RSpec.describe API::Projects do
|
|||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['message']['container_expiration_policy.name_regex_keep']).to contain_exactly('not valid RE2 syntax: missing ]: [')
|
||||
end
|
||||
|
||||
it "doesn't update container_expiration_policy with invalid keep_n" do
|
||||
project_param = {
|
||||
container_expiration_policy_attributes: {
|
||||
cadence: '1month',
|
||||
enabled: true,
|
||||
keep_n: 'not_int',
|
||||
name_regex_keep: 'foo.*'
|
||||
}
|
||||
}
|
||||
|
||||
put api("/projects/#{project3.id}", user4), params: project_param
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
expect(json_response['error']).to eq('container_expiration_policy_attributes[keep_n] is invalid')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authenticated as project developer' do
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module StubExperiments
|
||||
SUFFIX = Gitlab::Experimentation::Experiment::FEATURE_FLAG_SUFFIX
|
||||
|
||||
# Stub Experiment with `key: true/false`
|
||||
#
|
||||
# @param [Hash] experiment where key is feature name and value is boolean whether active or not.
|
||||
|
@ -11,6 +13,7 @@ module StubExperiments
|
|||
allow(Gitlab::Experimentation).to receive(:active?).and_call_original
|
||||
|
||||
experiments.each do |experiment_key, enabled|
|
||||
Feature.persist_used!("#{experiment_key}#{SUFFIX}")
|
||||
allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled }
|
||||
end
|
||||
end
|
||||
|
@ -25,6 +28,7 @@ module StubExperiments
|
|||
allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original
|
||||
|
||||
experiments.each do |experiment_key, enabled|
|
||||
Feature.persist_used!("#{experiment_key}#{SUFFIX}")
|
||||
allow(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, anything) { enabled }
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue