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
|
- .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:
|
||||||
|
|
|
@ -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';
|
||||||
|
|
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 = []) {
|
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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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: '#',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
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). |
|
| `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. |
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
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 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
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
|
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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
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', () => {
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 <script>alert('hi')</script>"`;
|
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>"`;
|
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';
|
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 = {
|
||||||
|
|
|
@ -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(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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue