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
- .rspec-ee-unit-geo-parallel
rspec-ee unit pg11 geo minimal:
extends:
- rspec-ee unit pg11 geo
- .minimal-rspec-tests
- .rails:rules:ee-only-unit:minimal
# FIXME: Temporarily disable geo minimal rspec jobs https://gitlab.com/gitlab-org/gitlab/-/issues/294212
#rspec-ee unit pg11 geo minimal:
# extends:
# - rspec-ee unit pg11 geo
# - .minimal-rspec-tests
# - .rails:rules:ee-only-unit:minimal
rspec-ee integration pg11 geo:
extends:
- .rspec-ee-base-geo-pg11
- .rails:rules:ee-only-integration
rspec-ee integration pg11 geo minimal:
extends:
- rspec-ee integration pg11 geo
- .minimal-rspec-tests
- .rails:rules:ee-only-integration:minimal
# FIXME: Temporarily disable geo minimal rspec jobs https://gitlab.com/gitlab-org/gitlab/-/issues/294212
#rspec-ee integration pg11 geo minimal:
# extends:
# - rspec-ee integration pg11 geo
# - .minimal-rspec-tests
# - .rails:rules:ee-only-integration:minimal
rspec-ee system pg11 geo:
extends:
- .rspec-ee-base-geo-pg11
- .rails:rules:ee-only-system
rspec-ee system pg11 geo minimal:
extends:
- rspec-ee system pg11 geo
- .minimal-rspec-tests
- .rails:rules:ee-only-system:minimal
# FIXME: Temporarily disable geo minimal rspec jobs https://gitlab.com/gitlab-org/gitlab/-/issues/294212
#rspec-ee system pg11 geo minimal:
# extends:
# - rspec-ee system pg11 geo
# - .minimal-rspec-tests
# - .rails:rules:ee-only-system:minimal
db:rollback geo:
extends:

View file

@ -10,3 +10,12 @@ export const CONTENT_UPDATE_DEBOUNCE = 250;
export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
'Editor Lite instance is required to set up an extension.',
);
//
// EXTENSIONS' CONSTANTS
//
// For CI config schemas the filename must match
// '*.gitlab-ci.yml' regardless of project configuration.
// https://gitlab.com/gitlab-org/gitlab/-/issues/293641
export const EXTENSION_CI_SCHEMA_FILE_NAME_MATCH = '.gitlab-ci.yml';

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 = []) {
if (!gon.features?.reviewerApprovalRules || !elsClassName?.includes('reviewer')) {
const count = approvalRules.length;
if (!gon.features?.reviewerApprovalRules || !elsClassName?.includes('reviewer') || !count) {
return '';
}
const count = approvalRules.length;
const [rule] = approvalRules;
const countText = sprintf(__('(+%{count} rules)'), { count });
const renderApprovalRulesCount = count > 1 ? `<span class="ml-1">${countText}</span>` : '';
const ruleName = rule.rule_type === 'code_owner' ? __('Code Owner') : rule.name;
return count
? `<div class="gl-display-flex gl-font-sm">
<span class="gl-text-truncate" title="${rule.name}">${rule.name}</span>
${renderApprovalRulesCount}
</div>`
: '';
return `<div class="gl-display-flex gl-font-sm">
<span class="gl-text-truncate" title="${ruleName}">${ruleName}</span>
${renderApprovalRulesCount}
</div>`;
};
export default UsersSelect;

View file

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

View file

@ -4,6 +4,7 @@ import {
GfmAutocompleteType,
tributeConfig,
} from 'ee_else_ce/vue_shared/components/gfm_autocomplete/utils';
import * as Emoji from '~/emoji';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@ -76,6 +77,14 @@ export default {
return (inputText, processValues) => {
if (this.cache[type]) {
processValues(this.filterValues(type));
} else if (type === GfmAutocompleteType.Emojis) {
Emoji.initEmojiMap()
.then(() => {
const emojis = Emoji.getValidEmojiNames();
this.cache[type] = emojis;
processValues(emojis);
})
.catch(() => createFlash({ message: this.$options.errorMessage }));
} else if (this.dataSources[type]) {
axios
.get(this.dataSources[type])

View file

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

View file

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

View file

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

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). |
| `builds_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
| `ci_config_path` | string | **{dotted-circle}** No | The path to CI configuration file. |
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
| `container_registry_enabled` | boolean | **{dotted-circle}** No | Enable container registry for this project. |
| `default_branch` | string | **{dotted-circle}** No | `master` by default. |
| `description` | string | **{dotted-circle}** No | Short project description. |
@ -1206,7 +1206,7 @@ PUT /projects/:id
| `ci_config_path` | string | **{dotted-circle}** No | The path to CI configuration file. |
| `ci_default_git_depth` | integer | **{dotted-circle}** No | Default number of revisions for [shallow cloning](../ci/pipelines/settings.md#git-shallow-clone). |
| `ci_forward_deployment_enabled` | boolean | **{dotted-circle}** No | When a new deployment job starts, [skip older deployment jobs](../ci/pipelines/settings.md#skip-outdated-deployment-jobs) that are still pending |
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (string), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
| `container_registry_enabled` | boolean | **{dotted-circle}** No | Enable container registry for this project. |
| `default_branch` | string | **{dotted-circle}** No | `master` by default. |
| `description` | string | **{dotted-circle}** No | Short project description. |

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.
### Best practices
- Avoid using [global keywords](../../ci/yaml/README.md#global-keywords),
such as `image`, `stages` and `variables` at top-level.
When a root `.gitlab-ci.yml` [includes](../../ci/yaml/README.md#include)
multiple templates, these global keywords could be overridden by the
others and cause an unexpected behavior.
## Versioning
Versioning allows you to introduce a new template without modifying the existing

View file

@ -20,6 +20,9 @@ IP rate limits**:
These limits are disabled by default.
NOTE:
By default, all Git operations are first tried unathenticated. Because of this, HTTP Git operations may trigger the rate limits configured for unauthenticated requests.
![user-and-ip-rate-limits](img/user_and_ip_rate_limits.png)
## Use an HTTP header to bypass rate limiting

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).
## Clone in Apple Xcode
## Clone a repository
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/45820) in GitLab 11.0
Learn how to [clone a repository through the command line](../../../gitlab-basics/start-using-git.md#clone-a-repository).
Alternatively, clone directly into a code editor as documented below.
### Clone to Apple Xcode
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/45820) in GitLab 11.0.
Projects that contain a `.xcodeproj` or `.xcworkspace` directory can now be cloned
in Xcode using the new **Open in Xcode** button, located next to the Git URL
into Xcode using the new **Open in Xcode** button, located next to the Git URL
used for cloning your project. The button is only shown on macOS.
## Download Source Code
> Support for directory download was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/24704) in GitLab 11.11.
> Support for [including Git LFS blobs](../../../topics/git/lfs#lfs-objects-in-project-archives) was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15079) in GitLab 13.5.
> - Support for directory download was [introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/24704) in GitLab 11.11.
> - Support for [including Git LFS blobs](../../../topics/git/lfs#lfs-objects-in-project-archives) was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15079) in GitLab 13.5.
The source code stored in a repository can be downloaded from the UI.
By clicking the download icon, a dropdown will open with links to download the following:

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 Entities
class ProjectRepositoryStorageMove < Grape::Entity
expose :id
expose :created_at
expose :human_state_name, as: :state
expose :source_storage_name
expose :destination_storage_name
class ProjectRepositoryStorageMove < BasicRepositoryStorageMove
expose :project, using: Entities::ProjectIdentity
end
end

View file

@ -2,18 +2,8 @@
module API
module Entities
class Snippet < Grape::Entity
expose :id, :title, :description, :visibility
class Snippet < BasicSnippet
expose :author, using: Entities::UserBasic
expose :updated_at, :created_at
expose :project_id
expose :web_url do |snippet|
Gitlab::UrlBuilder.build(snippet)
end
expose :raw_url do |snippet|
Gitlab::UrlBuilder.build(snippet, raw: true)
end
expose :ssh_url_to_repo, :http_url_to_repo, if: ->(snippet) { snippet.repository_exists? }
expose :file_name do |snippet|
snippet.file_name_on_repo || snippet.file_name
end

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
optional :cadence, type: String, desc: 'Container expiration policy cadence for recurring job'
optional :keep_n, type: String, desc: 'Container expiration policy number of images to keep'
optional :keep_n, type: Integer, desc: 'Container expiration policy number of images to keep'
optional :older_than, type: String, desc: 'Container expiration policy remove images older than value'
optional :name_regex, type: String, desc: 'Container expiration policy regex for image removal'
optional :name_regex_keep, type: String, desc: 'Container expiration policy regex for image retention'

View file

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

View file

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

View file

@ -3,6 +3,8 @@
module Gitlab
module Experimentation
class Experiment
FEATURE_FLAG_SUFFIX = "_experiment_percentage"
attr_reader :key, :tracking_category, :use_backwards_compatible_subject_index
def initialize(key, **params)
@ -10,7 +12,7 @@ module Gitlab
@tracking_category = params[:tracking_category]
@use_backwards_compatible_subject_index = params[:use_backwards_compatible_subject_index]
@experiment_percentage = Feature.get(:"#{key}_experiment_percentage").percentage_of_time_value # rubocop:disable Gitlab/AvoidFeatureGet
@experiment_percentage = Feature.get(:"#{key}#{FEATURE_FLAG_SUFFIX}").percentage_of_time_value # rubocop:disable Gitlab/AvoidFeatureGet
end
def active?

View file

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

View file

@ -426,36 +426,16 @@ RSpec.describe 'GFM autocomplete', :js do
visit project_issue_path(project, issue)
note = find('#note-body')
start_comment_with_emoji(note)
start_comment_with_emoji(note, '.atwho-view li')
start_and_cancel_discussion
note.fill_in(with: '')
start_comment_with_emoji(note)
start_comment_with_emoji(note, '.atwho-view li')
note.native.send_keys(:enter)
expect(note.value).to eql('Hello :100: ')
end
def start_comment_with_emoji(note)
note.native.send_keys('Hello :10')
wait_for_requests
find('.atwho-view li', text: '100')
end
def start_and_cancel_discussion
click_button('Reply...')
fill_in('note_note', with: 'Whoops!')
page.accept_alert 'Are you sure you want to cancel creating this comment?' do
click_button('Cancel')
end
wait_for_requests
end
end
shared_examples 'autocomplete suggestions' do
@ -599,6 +579,33 @@ RSpec.describe 'GFM autocomplete', :js do
expect(page).not_to have_selector('.tribute-container', visible: true)
end
it 'does not open autocomplete menu when ":" is prefixed by a number and letters' do
note = find('#note-body')
# Number.
page.within '.timeline-content-form' do
note.native.send_keys('7:')
end
expect(page).not_to have_selector('.tribute-container', visible: true)
# ASCII letter.
page.within '.timeline-content-form' do
note.set('')
note.native.send_keys('w:')
end
expect(page).not_to have_selector('.tribute-container', visible: true)
# Non-ASCII letter.
page.within '.timeline-content-form' do
note.set('')
note.native.send_keys('Ё:')
end
expect(page).not_to have_selector('.tribute-container', visible: true)
end
it 'selects the first item for assignee dropdowns' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys('@')
@ -624,6 +631,16 @@ RSpec.describe 'GFM autocomplete', :js do
expect(find('.tribute-container ul', visible: true)).to have_content(user.name)
end
it 'selects the first item for non-assignee dropdowns if a query is entered' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys(':1')
end
wait_for_requests
expect(find('.tribute-container ul', visible: true)).to have_selector('.highlight:first-of-type')
end
context 'when autocompleting for groups' do
it 'shows the group when searching for the name of the group' do
page.within '.timeline-content-form' do
@ -687,6 +704,25 @@ RSpec.describe 'GFM autocomplete', :js do
expect_to_wrap(false, user_item, note, user.username)
end
it 'does not wrap for emoji values' do
note = find('#note-body')
page.within '.timeline-content-form' do
note.native.send_keys(":cartwheel_")
end
emoji_item = first('.tribute-container li', text: 'cartwheel_tone1', visible: true)
expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
end
it 'does not open autocomplete if there is no space before' do
page.within '.timeline-content-form' do
find('#note-body').native.send_keys("hello:#{user.username[0..2]}")
end
expect(page).not_to have_selector('.tribute-container')
end
it 'triggers autocomplete after selecting a quick action' do
note = find('#note-body')
page.within '.timeline-content-form' do
@ -824,6 +860,26 @@ RSpec.describe 'GFM autocomplete', :js do
end
end
context 'when other notes are destroyed' do
let!(:discussion) { create(:discussion_note_on_issue, noteable: issue, project: issue.project) }
# This is meant to protect against this issue https://gitlab.com/gitlab-org/gitlab/-/issues/228729
it 'keeps autocomplete key listeners' do
visit project_issue_path(project, issue)
note = find('#note-body')
start_comment_with_emoji(note, '.tribute-container li')
start_and_cancel_discussion
note.fill_in(with: '')
start_comment_with_emoji(note, '.tribute-container li')
note.native.send_keys(:enter)
expect(note.value).to eql('Hello :100: ')
end
end
shared_examples 'autocomplete suggestions' do
it 'suggests objects correctly' do
page.within '.timeline-content-form' do
@ -913,4 +969,24 @@ RSpec.describe 'GFM autocomplete', :js do
note.native.send_keys(text)
end
end
def start_comment_with_emoji(note, selector)
note.native.send_keys('Hello :10')
wait_for_requests
find(selector, text: '100')
end
def start_and_cancel_discussion
click_button('Reply...')
fill_in('note_note', with: 'Whoops!')
page.accept_alert 'Are you sure you want to cancel creating this comment?' do
click_button('Cancel')
end
wait_for_requests
end
end

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

View file

@ -1,5 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`gfm_autocomplete/utils emojis config shows the emoji name and icon in the menu item 1`] = `
"raised_hands
<gl-emoji
data-name=\\"raised_hands\\"></gl-emoji>
"
`;
exports[`gfm_autocomplete/utils issues config shows the iid and title in the menu item within a project context 1`] = `"<small>123456</small> Project context issue title &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';
describe('gfm_autocomplete/utils', () => {
describe('emojis config', () => {
const emojisConfig = tributeConfig[GfmAutocompleteType.Emojis].config;
const emoji = 'raised_hands';
it('uses : as the trigger', () => {
expect(emojisConfig.trigger).toBe(':');
});
it('searches using the emoji name', () => {
expect(emojisConfig.lookup(emoji)).toBe(emoji);
});
it('shows the emoji name and icon in the menu item', () => {
expect(emojisConfig.menuItemTemplate({ original: emoji })).toMatchSnapshot();
});
it('inserts the emoji name on autocomplete selection', () => {
expect(emojisConfig.selectTemplate({ original: emoji })).toBe(`:${emoji}:`);
});
});
describe('issues config', () => {
const issuesConfig = tributeConfig[GfmAutocompleteType.Issues].config;
const groupContextIssue = {

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(json_response['message']['container_expiration_policy.name_regex_keep']).to contain_exactly('not valid RE2 syntax: missing ]: [')
end
it "doesn't update container_expiration_policy with invalid keep_n" do
project_param = {
container_expiration_policy_attributes: {
cadence: '1month',
enabled: true,
keep_n: 'not_int',
name_regex_keep: 'foo.*'
}
}
put api("/projects/#{project3.id}", user4), params: project_param
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('container_expiration_policy_attributes[keep_n] is invalid')
end
end
context 'when authenticated as project developer' do

View file

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