diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 2818b6be176..30c4fc10f94 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -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:
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index d6f87872bde..8d1a3d17c6e 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -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';
diff --git a/app/assets/javascripts/editor/editor_ci_schema_ext.js b/app/assets/javascripts/editor/editor_ci_schema_ext.js
new file mode 100644
index 00000000000..1946be997a7
--- /dev/null
+++ b/app/assets/javascripts/editor/editor_ci_schema_ext.js
@@ -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],
+ });
+ }
+}
diff --git a/app/assets/javascripts/users_select/index.js b/app/assets/javascripts/users_select/index.js
index e693c3e90a4..9278e0c5859 100644
--- a/app/assets/javascripts/users_select/index.js
+++ b/app/assets/javascripts/users_select/index.js
@@ -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 ? `${countText}` : '';
+ const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : rule.name;
- return count
- ? `
- ${rule.name}
- ${renderApprovalRulesCount}
-
`
- : '';
+ return `
+ ${ruleName}
+ ${renderApprovalRulesCount}
+
`;
};
export default UsersSelect;
diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/editor_lite.vue
index cfe3ce0a11c..7218b84cf8a 100644
--- a/app/assets/javascripts/vue_shared/components/editor_lite.vue
+++ b/app/assets/javascripts/vue_shared/components/editor_lite.vue
@@ -84,6 +84,9 @@ export default {
onFileChange() {
this.$emit('input', this.editor.getValue());
},
+ getEditor() {
+ return this.editor;
+ },
},
};
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
index 1ad0ca36bf8..02d89175ceb 100644
--- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue
@@ -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])
diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
index 2581888b504..9fab8400ec4 100644
--- a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
+++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js
@@ -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: '#',
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 232a3054cd0..d977a034002 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -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,
diff --git a/app/assets/stylesheets/pages/trials.scss b/app/assets/stylesheets/pages/trials.scss
index 3fb9054b2b8..55f323b7df7 100644
--- a/app/assets/stylesheets/pages/trials.scss
+++ b/app/assets/stylesheets/pages/trials.scss
@@ -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;
}
diff --git a/changelogs/unreleased/keep_n_int.yml b/changelogs/unreleased/keep_n_int.yml
new file mode 100644
index 00000000000..067308833a6
--- /dev/null
+++ b/changelogs/unreleased/keep_n_int.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure container_expiration_policy keep_n is an integer
+merge_request: 49805
+author: Mathieu Parent
+type: changed
diff --git a/config/feature_flags/experiment/null_hypothesis.yml b/config/feature_flags/experiment/null_hypothesis.yml
deleted file mode 100644
index 716b0711ef1..00000000000
--- a/config/feature_flags/experiment/null_hypothesis.yml
+++ /dev/null
@@ -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
diff --git a/doc/api/projects.md b/doc/api/projects.md
index b9f6448085d..319290c3e19 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -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. |
diff --git a/doc/development/cicd/templates.md b/doc/development/cicd/templates.md
index 1ab569ba0df..bb2ed11b4d4 100644
--- a/doc/development/cicd/templates.md
+++ b/doc/development/cicd/templates.md
@@ -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
diff --git a/doc/user/admin_area/settings/user_and_ip_rate_limits.md b/doc/user/admin_area/settings/user_and_ip_rate_limits.md
index 3f0d75dc682..3289a201890 100644
--- a/doc/user/admin_area/settings/user_and_ip_rate_limits.md
+++ b/doc/user/admin_area/settings/user_and_ip_rate_limits.md
@@ -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
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index e1d84baec4d..880e0f2f805 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -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:
diff --git a/lib/api/entities/basic_repository_storage_move.rb b/lib/api/entities/basic_repository_storage_move.rb
new file mode 100644
index 00000000000..3ee112fb9a2
--- /dev/null
+++ b/lib/api/entities/basic_repository_storage_move.rb
@@ -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
diff --git a/lib/api/entities/basic_snippet.rb b/lib/api/entities/basic_snippet.rb
new file mode 100644
index 00000000000..26297514798
--- /dev/null
+++ b/lib/api/entities/basic_snippet.rb
@@ -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
diff --git a/lib/api/entities/project_repository_storage_move.rb b/lib/api/entities/project_repository_storage_move.rb
index 25643651a14..191bbaf19d7 100644
--- a/lib/api/entities/project_repository_storage_move.rb
+++ b/lib/api/entities/project_repository_storage_move.rb
@@ -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
diff --git a/lib/api/entities/snippet.rb b/lib/api/entities/snippet.rb
index 85148c03d18..f05e593a302 100644
--- a/lib/api/entities/snippet.rb
+++ b/lib/api/entities/snippet.rb
@@ -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
diff --git a/lib/api/entities/snippet_repository_storage_move.rb b/lib/api/entities/snippet_repository_storage_move.rb
new file mode 100644
index 00000000000..ee86816bd14
--- /dev/null
+++ b/lib/api/entities/snippet_repository_storage_move.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class SnippetRepositoryStorageMove < BasicRepositoryStorageMove
+ expose :snippet, using: Entities::BasicSnippet
+ end
+ end
+end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index f5f45cf7351..3115e968e84 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -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'
diff --git a/lib/feature/shared.rb b/lib/feature/shared.rb
index 17dfe26bd82..a3f02156d94 100644
--- a/lib/feature/shared.rb
+++ b/lib/feature/shared.rb
@@ -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
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 94523813662..191a1fb1b6f 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -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
diff --git a/lib/gitlab/experimentation/experiment.rb b/lib/gitlab/experimentation/experiment.rb
index e594c3bedeb..b9eda0cfc31 100644
--- a/lib/gitlab/experimentation/experiment.rb
+++ b/lib/gitlab/experimentation/experiment.rb
@@ -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?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0fd057100b0..cd622919149 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 07bf821a590..457af4a3e7c 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -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
diff --git a/spec/frontend/editor/editor_ci_schema_ext_spec.js b/spec/frontend/editor/editor_ci_schema_ext_spec.js
new file mode 100644
index 00000000000..f9a1070f601
--- /dev/null
+++ b/spec/frontend/editor/editor_ci_schema_ext_spec.js
@@ -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('');
+ 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'],
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/editor_lite_spec.js b/spec/frontend/vue_shared/components/editor_lite_spec.js
index 52502fcf64f..cc11f76d01f 100644
--- a/spec/frontend/vue_shared/components/editor_lite_spec.js
+++ b/spec/frontend/vue_shared/components/editor_lite_spec.js
@@ -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();
});
});
});
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
index d0fa2086fdc..b8dba8e24e7 100644
--- a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
@@ -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
+
+ "
+`;
+
exports[`gfm_autocomplete/utils issues config shows the iid and title in the menu item within a project context 1`] = `"123456 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`] = `"gitlab#987654 Group context issue title <script>alert('hi')</script>"`;
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js
index 647f8c6e000..a66a6d79c83 100644
--- a/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js
@@ -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 = {
diff --git a/spec/lib/api/entities/snippet_repository_storage_move_spec.rb b/spec/lib/api/entities/snippet_repository_storage_move_spec.rb
new file mode 100644
index 00000000000..8086be3ffa7
--- /dev/null
+++ b/spec/lib/api/entities/snippet_repository_storage_move_spec.rb
@@ -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
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index ad5468fb54c..53c0e95c777 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -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
diff --git a/spec/support/helpers/stub_experiments.rb b/spec/support/helpers/stub_experiments.rb
index 247692d83ee..9188bc704c1 100644
--- a/spec/support/helpers/stub_experiments.rb
+++ b/spec/support/helpers/stub_experiments.rb
@@ -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