Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-12-17 12:09:57 +00:00
parent 173bd0618f
commit 4e8387dc14
33 changed files with 507 additions and 115 deletions

View file

@ -549,33 +549,36 @@ rspec-ee unit pg11 geo:
- .rails:rules:ee-only-unit - .rails:rules:ee-only-unit
- .rspec-ee-unit-geo-parallel - .rspec-ee-unit-geo-parallel
rspec-ee unit pg11 geo minimal: # FIXME: Temporarily disable geo minimal rspec jobs https://gitlab.com/gitlab-org/gitlab/-/issues/294212
extends: #rspec-ee unit pg11 geo minimal:
- rspec-ee unit pg11 geo # extends:
- .minimal-rspec-tests # - rspec-ee unit pg11 geo
- .rails:rules:ee-only-unit:minimal # - .minimal-rspec-tests
# - .rails:rules:ee-only-unit:minimal
rspec-ee integration pg11 geo: rspec-ee integration pg11 geo:
extends: extends:
- .rspec-ee-base-geo-pg11 - .rspec-ee-base-geo-pg11
- .rails:rules:ee-only-integration - .rails:rules:ee-only-integration
rspec-ee integration pg11 geo minimal: # FIXME: Temporarily disable geo minimal rspec jobs https://gitlab.com/gitlab-org/gitlab/-/issues/294212
extends: #rspec-ee integration pg11 geo minimal:
- rspec-ee integration pg11 geo # extends:
- .minimal-rspec-tests # - rspec-ee integration pg11 geo
- .rails:rules:ee-only-integration:minimal # - .minimal-rspec-tests
# - .rails:rules:ee-only-integration:minimal
rspec-ee system pg11 geo: rspec-ee system pg11 geo:
extends: extends:
- .rspec-ee-base-geo-pg11 - .rspec-ee-base-geo-pg11
- .rails:rules:ee-only-system - .rails:rules:ee-only-system
rspec-ee system pg11 geo minimal: # FIXME: Temporarily disable geo minimal rspec jobs https://gitlab.com/gitlab-org/gitlab/-/issues/294212
extends: #rspec-ee system pg11 geo minimal:
- rspec-ee system pg11 geo # extends:
- .minimal-rspec-tests # - rspec-ee system pg11 geo
- .rails:rules:ee-only-system:minimal # - .minimal-rspec-tests
# - .rails:rules:ee-only-system:minimal
db:rollback geo: db:rollback geo:
extends: extends:

View file

@ -10,3 +10,12 @@ export const CONTENT_UPDATE_DEBOUNCE = 250;
export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __( export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
'Editor Lite instance is required to set up an 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';

View 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],
});
}
}

View file

@ -832,21 +832,21 @@ UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
}; };
UsersSelect.prototype.renderApprovalRules = function(elsClassName, approvalRules = []) { 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 ''; return '';
} }
const count = approvalRules.length;
const [rule] = approvalRules; const [rule] = approvalRules;
const countText = sprintf(__('(+%{count} rules)'), { count }); const countText = sprintf(__('(+%{count} rules)'), { count });
const renderApprovalRulesCount = count > 1 ? `<span class="ml-1">${countText}</span>` : ''; const renderApprovalRulesCount = count > 1 ? `<span class="ml-1">${countText}</span>` : '';
const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : rule.name;
return count return `<div class="gl-display-flex gl-font-sm">
? `<div class="gl-display-flex gl-font-sm"> <span class="gl-text-truncate" title="${ruleName}">${ruleName}</span>
<span class="gl-text-truncate" title="${rule.name}">${rule.name}</span> ${renderApprovalRulesCount}
${renderApprovalRulesCount} </div>`;
</div>`
: '';
}; };
export default UsersSelect; export default UsersSelect;

View file

@ -84,6 +84,9 @@ export default {
onFileChange() { onFileChange() {
this.$emit('input', this.editor.getValue()); this.$emit('input', this.editor.getValue());
}, },
getEditor() {
return this.editor;
},
}, },
}; };
</script> </script>

View file

@ -4,6 +4,7 @@ import {
GfmAutocompleteType, GfmAutocompleteType,
tributeConfig, tributeConfig,
} from 'ee_else_ce/vue_shared/components/gfm_autocomplete/utils'; } from 'ee_else_ce/vue_shared/components/gfm_autocomplete/utils';
import * as Emoji from '~/emoji';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
@ -76,6 +77,14 @@ export default {
return (inputText, processValues) => { return (inputText, processValues) => {
if (this.cache[type]) { if (this.cache[type]) {
processValues(this.filterValues(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]) { } else if (this.dataSources[type]) {
axios axios
.get(this.dataSources[type]) .get(this.dataSources[type])

View file

@ -1,4 +1,5 @@
import { escape, last } from 'lodash'; import { escape, last } from 'lodash';
import * as Emoji from '~/emoji';
import { spriteIcon } from '~/lib/utils/common_utils'; import { spriteIcon } from '~/lib/utils/common_utils';
const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings 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+$/; const nonWordOrInteger = /\W|^\d+$/;
export const GfmAutocompleteType = { export const GfmAutocompleteType = {
Emojis: 'emojis',
Issues: 'issues', Issues: 'issues',
Labels: 'labels', Labels: 'labels',
Members: 'members', Members: 'members',
@ -21,6 +23,15 @@ function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
} }
export const tributeConfig = { export const tributeConfig = {
[GfmAutocompleteType.Emojis]: {
config: {
trigger: ':',
lookup: value => value,
menuItemTemplate: ({ original }) => `${original} ${Emoji.glEmojiTag(original)}`,
selectTemplate: ({ original }) => `:${original}:`,
},
},
[GfmAutocompleteType.Issues]: { [GfmAutocompleteType.Issues]: {
config: { config: {
trigger: '#', trigger: '#',

View file

@ -169,7 +169,7 @@ export default {
return new GLForm( return new GLForm(
$(this.$refs['gl-form']), $(this.$refs['gl-form']),
{ {
emojis: this.enableAutocomplete, emojis: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, members: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, issues: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,
mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete, mergeRequests: this.enableAutocomplete && !this.glFeatures.tributeAutocomplete,

View file

@ -3,13 +3,13 @@
* MR link https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22716 * MR link https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22716
*/ */
.gl-select2-html5-required-fix div.select2-container+select.select2 { .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; display: block !important;
width: 1px; width: 1px;
height: 1px; height: 1px;
z-index: -1; z-index: -1;
opacity: 0;
margin: -3px auto 0; margin: -3px auto 0;
background-image: none;
background-color: transparent;
border: 0;
} }

View file

@ -0,0 +1,5 @@
---
title: Ensure container_expiration_policy keep_n is an integer
merge_request: 49805
author: Mathieu Parent
type: changed

View file

@ -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

View file

@ -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). | | `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`. | | `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. | | `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. | | `container_registry_enabled` | boolean | **{dotted-circle}** No | Enable container registry for this project. |
| `default_branch` | string | **{dotted-circle}** No | `master` by default. | | `default_branch` | string | **{dotted-circle}** No | `master` by default. |
| `description` | string | **{dotted-circle}** No | Short project description. | | `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_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_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 | | `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. | | `container_registry_enabled` | boolean | **{dotted-circle}** No | Enable container registry for this project. |
| `default_branch` | string | **{dotted-circle}** No | `master` by default. | | `default_branch` | string | **{dotted-circle}** No | `master` by default. |
| `description` | string | **{dotted-circle}** No | Short project description. | | `description` | string | **{dotted-circle}** No | Short project description. |

View file

@ -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. 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
Versioning allows you to introduce a new template without modifying the existing Versioning allows you to introduce a new template without modifying the existing

View file

@ -20,6 +20,9 @@ IP rate limits**:
These limits are disabled by default. 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) ![user-and-ip-rate-limits](img/user_and_ip_rate_limits.png)
## Use an HTTP header to bypass rate limiting ## Use an HTTP header to bypass rate limiting

View file

@ -236,18 +236,24 @@ lock your files to prevent any conflicting changes.
You can access your repositories via [repository API](../../../api/repositories.md). 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 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. used for cloning your project. The button is only shown on macOS.
## Download Source Code ## Download Source Code
> Support for directory download was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/24704) in GitLab 11.11. > - 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 [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. 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: By clicking the download icon, a dropdown will open with links to download the following:

View 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

View 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

View file

@ -2,12 +2,7 @@
module API module API
module Entities module Entities
class ProjectRepositoryStorageMove < Grape::Entity class ProjectRepositoryStorageMove < BasicRepositoryStorageMove
expose :id
expose :created_at
expose :human_state_name, as: :state
expose :source_storage_name
expose :destination_storage_name
expose :project, using: Entities::ProjectIdentity expose :project, using: Entities::ProjectIdentity
end end
end end

View file

@ -2,18 +2,8 @@
module API module API
module Entities module Entities
class Snippet < Grape::Entity class Snippet < BasicSnippet
expose :id, :title, :description, :visibility
expose :author, using: Entities::UserBasic 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| expose :file_name do |snippet|
snippet.file_name_on_repo || snippet.file_name snippet.file_name_on_repo || snippet.file_name
end end

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
module API
module Entities
class SnippetRepositoryStorageMove < BasicRepositoryStorageMove
expose :snippet, using: Entities::BasicSnippet
end
end
end

View file

@ -99,7 +99,7 @@ module API
params :optional_container_expiration_policy_params do params :optional_container_expiration_policy_params do
optional :cadence, type: String, desc: 'Container expiration policy cadence for recurring job' 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 :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, type: String, desc: 'Container expiration policy regex for image removal'
optional :name_regex_keep, type: String, desc: 'Container expiration policy regex for image retention' optional :name_regex_keep, type: String, desc: 'Container expiration policy regex for image retention'

View file

@ -57,6 +57,8 @@ class Feature
default_enabled: false, default_enabled: false,
example: <<-EOS example: <<-EOS
experiment(:my_experiment, project: project, actor: current_user) { ...variant code... } experiment(:my_experiment, project: project, actor: current_user) { ...variant code... }
# or
Gitlab::Experimentation.in_experiment_group?(:my_experiment, subject: current_user)
EOS EOS
} }
}.freeze }.freeze

View file

@ -87,6 +87,9 @@ module Gitlab
}, },
invite_members_empty_project_version_a: { invite_members_empty_project_version_a: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyProjectVersionA' tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyProjectVersionA'
},
trial_during_signup: {
tracking_category: 'Growth::Conversion::Experiment::TrialDuringSignup'
} }
}.freeze }.freeze

View file

@ -3,6 +3,8 @@
module Gitlab module Gitlab
module Experimentation module Experimentation
class Experiment class Experiment
FEATURE_FLAG_SUFFIX = "_experiment_percentage"
attr_reader :key, :tracking_category, :use_backwards_compatible_subject_index attr_reader :key, :tracking_category, :use_backwards_compatible_subject_index
def initialize(key, **params) def initialize(key, **params)
@ -10,7 +12,7 @@ module Gitlab
@tracking_category = params[:tracking_category] @tracking_category = params[:tracking_category]
@use_backwards_compatible_subject_index = params[:use_backwards_compatible_subject_index] @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 end
def active? def active?

View file

@ -2820,9 +2820,6 @@ msgstr ""
msgid "All changes are committed" msgid "All changes are committed"
msgstr "" msgstr ""
msgid "All default stages are currently visible"
msgstr ""
msgid "All email addresses will be used to identify your commits." msgid "All email addresses will be used to identify your commits."
msgstr "" msgstr ""
@ -6910,6 +6907,9 @@ msgstr ""
msgid "Code Coverage|Couldn't fetch the code coverage data" msgid "Code Coverage|Couldn't fetch the code coverage data"
msgstr "" msgstr ""
msgid "Code Owner"
msgstr ""
msgid "Code Owners" msgid "Code Owners"
msgstr "" msgstr ""
@ -7329,6 +7329,9 @@ msgstr ""
msgid "Congratulations! You have enabled Two-factor Authentication!" msgid "Congratulations! You have enabled Two-factor Authentication!"
msgstr "" msgstr ""
msgid "Congratulations, your free trial is activated."
msgstr ""
msgid "Connect" msgid "Connect"
msgstr "" msgstr ""
@ -8330,9 +8333,21 @@ msgstr ""
msgid "CustomCycleAnalytics|Add stage" msgid "CustomCycleAnalytics|Add stage"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|All default stages are currently visible"
msgstr ""
msgid "CustomCycleAnalytics|Default stages"
msgstr ""
msgid "CustomCycleAnalytics|Editing stage" msgid "CustomCycleAnalytics|Editing stage"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|End event"
msgstr ""
msgid "CustomCycleAnalytics|End event label"
msgstr ""
msgid "CustomCycleAnalytics|Enter a name for the stage" msgid "CustomCycleAnalytics|Enter a name for the stage"
msgstr "" msgstr ""
@ -8345,10 +8360,13 @@ msgstr ""
msgid "CustomCycleAnalytics|Please select a start event first" msgid "CustomCycleAnalytics|Please select a start event first"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Select start event" msgid "CustomCycleAnalytics|Recover hidden stage"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Select stop event" msgid "CustomCycleAnalytics|Select end event"
msgstr ""
msgid "CustomCycleAnalytics|Select start event"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Stage name already exists" msgid "CustomCycleAnalytics|Stage name already exists"
@ -8357,18 +8375,12 @@ msgstr ""
msgid "CustomCycleAnalytics|Start event" msgid "CustomCycleAnalytics|Start event"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Start event changed, please select a valid stop event" msgid "CustomCycleAnalytics|Start event changed, please select a valid end event"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Start event label" msgid "CustomCycleAnalytics|Start event label"
msgstr "" msgstr ""
msgid "CustomCycleAnalytics|Stop event"
msgstr ""
msgid "CustomCycleAnalytics|Stop event label"
msgstr ""
msgid "CustomCycleAnalytics|Update stage" msgid "CustomCycleAnalytics|Update stage"
msgstr "" msgstr ""
@ -8959,9 +8971,6 @@ msgstr ""
msgid "Default projects limit" msgid "Default projects limit"
msgstr "" msgstr ""
msgid "Default stages"
msgstr ""
msgid "Default: Map a FogBugz account ID to a full name" msgid "Default: Map a FogBugz account ID to a full name"
msgstr "" msgstr ""
@ -12977,6 +12986,9 @@ msgstr ""
msgid "GitLab Billing Team." msgid "GitLab Billing Team."
msgstr "" msgstr ""
msgid "GitLab Gold trial (optional)"
msgstr ""
msgid "GitLab Group Runners can execute code for all the projects in this group." msgid "GitLab Group Runners can execute code for all the projects in this group."
msgstr "" msgstr ""
@ -14223,6 +14235,9 @@ msgstr ""
msgid "How many days need to pass between marking entity for deletion and actual removing it." msgid "How many days need to pass between marking entity for deletion and actual removing it."
msgstr "" msgstr ""
msgid "How many employees will use Gitlab?"
msgstr ""
msgid "How many replicas each Elasticsearch shard has." msgid "How many replicas each Elasticsearch shard has."
msgstr "" 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'" 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 "" msgstr ""
msgid "Trial|Dismiss"
msgstr ""
msgid "Trial|Successful trial activation image"
msgstr ""
msgid "Trigger" msgid "Trigger"
msgstr "" msgstr ""
@ -31055,6 +31076,9 @@ msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot." msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr "" 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." msgid "We will notify %{inviter} that you declined their invitation to join GitLab. You will stop receiving reminders."
msgstr "" msgstr ""

View file

@ -426,36 +426,16 @@ RSpec.describe 'GFM autocomplete', :js do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
note = find('#note-body') note = find('#note-body')
start_comment_with_emoji(note) start_comment_with_emoji(note, '.atwho-view li')
start_and_cancel_discussion start_and_cancel_discussion
note.fill_in(with: '') note.fill_in(with: '')
start_comment_with_emoji(note) start_comment_with_emoji(note, '.atwho-view li')
note.native.send_keys(:enter) note.native.send_keys(:enter)
expect(note.value).to eql('Hello :100: ') expect(note.value).to eql('Hello :100: ')
end 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 end
shared_examples 'autocomplete suggestions' do 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) expect(page).not_to have_selector('.tribute-container', visible: true)
end 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 it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@') 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) expect(find('.tribute-container ul', visible: true)).to have_content(user.name)
end 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 context 'when autocompleting for groups' do
it 'shows the group when searching for the name of the group' do it 'shows the group when searching for the name of the group' do
page.within '.timeline-content-form' 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) expect_to_wrap(false, user_item, note, user.username)
end 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 it 'triggers autocomplete after selecting a quick action' do
note = find('#note-body') note = find('#note-body')
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
@ -824,6 +860,26 @@ RSpec.describe 'GFM autocomplete', :js do
end end
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 shared_examples 'autocomplete suggestions' do
it 'suggests objects correctly' do it 'suggests objects correctly' do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
@ -913,4 +969,24 @@ RSpec.describe 'GFM autocomplete', :js do
note.native.send_keys(text) note.native.send_keys(text)
end end
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 end

View 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'],
});
});
});
});
});

View file

@ -7,20 +7,22 @@ jest.mock('~/editor/editor_lite');
describe('Editor Lite component', () => { describe('Editor Lite component', () => {
let wrapper; let wrapper;
const onDidChangeModelContent = jest.fn(); let mockInstance;
const updateModelLanguage = jest.fn();
const getValue = jest.fn();
const setValue = jest.fn();
const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'; const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
const fileName = 'lorem.txt'; const fileName = 'lorem.txt';
const fileGlobalId = 'snippet_777'; const fileGlobalId = 'snippet_777';
const createInstanceMock = jest.fn().mockImplementation(() => ({ const createInstanceMock = jest.fn().mockImplementation(() => {
onDidChangeModelContent, mockInstance = {
updateModelLanguage, onDidChangeModelContent: jest.fn(),
getValue, updateModelLanguage: jest.fn(),
setValue, getValue: jest.fn(),
dispose: jest.fn(), setValue: jest.fn(),
})); dispose: jest.fn(),
};
return mockInstance;
});
Editor.mockImplementation(() => { Editor.mockImplementation(() => {
return { return {
createInstance: createInstanceMock, createInstance: createInstanceMock,
@ -46,8 +48,8 @@ describe('Editor Lite component', () => {
}); });
const triggerChangeContent = val => { const triggerChangeContent = val => {
getValue.mockReturnValue(val); mockInstance.getValue.mockReturnValue(val);
const [cb] = onDidChangeModelContent.mock.calls[0]; const [cb] = mockInstance.onDidChangeModelContent.mock.calls[0];
cb(); cb();
@ -92,12 +94,12 @@ describe('Editor Lite component', () => {
}); });
return nextTick().then(() => { return nextTick().then(() => {
expect(updateModelLanguage).toHaveBeenCalledWith(newFileName); expect(mockInstance.updateModelLanguage).toHaveBeenCalledWith(newFileName);
}); });
}); });
it('registers callback with editor onChangeContent', () => { 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', () => { it('emits input event when the blob content is changed', () => {
@ -117,6 +119,10 @@ describe('Editor Lite component', () => {
expect(wrapper.emitted()['editor-ready']).toBeDefined(); 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', () => { describe('reaction to the value update', () => {
it('reacts to the changes in the passed value', async () => { it('reacts to the changes in the passed value', async () => {
const newValue = 'New Value'; const newValue = 'New Value';
@ -126,7 +132,7 @@ describe('Editor Lite component', () => {
}); });
await nextTick(); 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 () => { 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(); await nextTick();
expect(setValue).not.toHaveBeenCalled(); expect(mockInstance.setValue).not.toHaveBeenCalled();
}); });
}); });
}); });

View file

@ -1,5 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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 &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`; 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 &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;
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 &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`; 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 &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;"`;

View file

@ -2,6 +2,27 @@ import { escape, last } from 'lodash';
import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils'; import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils';
describe('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', () => { describe('issues config', () => {
const issuesConfig = tributeConfig[GfmAutocompleteType.Issues].config; const issuesConfig = tributeConfig[GfmAutocompleteType.Issues].config;
const groupContextIssue = { const groupContextIssue = {

View file

@ -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

View file

@ -2711,6 +2711,22 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:bad_request) 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 ]: [') expect(json_response['message']['container_expiration_policy.name_regex_keep']).to contain_exactly('not valid RE2 syntax: missing ]: [')
end 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 end
context 'when authenticated as project developer' do context 'when authenticated as project developer' do

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module StubExperiments module StubExperiments
SUFFIX = Gitlab::Experimentation::Experiment::FEATURE_FLAG_SUFFIX
# Stub Experiment with `key: true/false` # Stub Experiment with `key: true/false`
# #
# @param [Hash] experiment where key is feature name and value is boolean whether active or not. # @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 allow(Gitlab::Experimentation).to receive(:active?).and_call_original
experiments.each do |experiment_key, enabled| experiments.each do |experiment_key, enabled|
Feature.persist_used!("#{experiment_key}#{SUFFIX}")
allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled } allow(Gitlab::Experimentation).to receive(:active?).with(experiment_key) { enabled }
end end
end end
@ -25,6 +28,7 @@ module StubExperiments
allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original allow(Gitlab::Experimentation).to receive(:in_experiment_group?).and_call_original
experiments.each do |experiment_key, enabled| 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 } allow(Gitlab::Experimentation).to receive(:in_experiment_group?).with(experiment_key, anything) { enabled }
end end
end end