diff options
35 files changed, 807 insertions, 43 deletions
diff --git a/app/assets/javascripts/behaviors/markdown/nodes/playable.js b/app/assets/javascripts/behaviors/markdown/nodes/playable.js index 64bc30e1fe7..2b667aba2d6 100644 --- a/app/assets/javascripts/behaviors/markdown/nodes/playable.js +++ b/app/assets/javascripts/behaviors/markdown/nodes/playable.js @@ -1,4 +1,5 @@ /* eslint-disable class-methods-use-this */ +/* eslint-disable @gitlab/require-i18n-strings */ import { defaultMarkdownSerializer } from 'prosemirror-markdown'; import { Node } from 'tiptap'; @@ -30,19 +31,19 @@ export default class Playable extends Node { const parseDOM = [ { - tag: `.media-container`, + tag: `.${this.mediaType}-container`, getAttrs: (el) => ({ - src: el.querySelector('audio,video').src, - alt: el.querySelector('audio,video').dataset.title, + src: el.querySelector(this.mediaType).src, + alt: el.querySelector(this.mediaType).dataset.title, }), }, ]; const toDOM = (node) => [ 'span', - { class: 'media-container' }, + { class: `media-container ${this.mediaType}-container` }, [ - this.options.mediaType, + this.mediaType, { src: node.attrs.src, controls: true, diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 1ca1cf512a1..cfcdd470cf4 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -3,7 +3,6 @@ import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui'; import { MountingPortal } from 'portal-vue'; import { mapActions, mapState, mapGetters } from 'vuex'; import { LIST, ListType, ListTypeTitles } from '~/boards/constants'; -import boardsStore from '~/boards/stores/boards_store'; import { isScopedLabel } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import eventHub from '~/sidebar/event_hub'; @@ -23,7 +22,7 @@ export default { import('ee_component/boards/components/board_settings_list_types.vue'), }, mixins: [glFeatureFlagMixin(), Tracking.mixin()], - inject: ['canAdminList'], + inject: ['canAdminList', 'scopedLabelsAvailable'], inheritAttrs: false, data() { return { @@ -61,7 +60,7 @@ export default { methods: { ...mapActions(['unsetActiveId', 'removeList']), showScopedLabels(label) { - return boardsStore.scopedLabels.enabled && isScopedLabel(label); + return this.scopedLabelsAvailable && isScopedLabel(label); }, deleteBoard() { // eslint-disable-next-line no-alert diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue index 30e304b8a65..f39e4d90357 100644 --- a/app/assets/javascripts/boards/components/config_toggle.vue +++ b/app/assets/javascripts/boards/components/config_toggle.vue @@ -15,11 +15,6 @@ export default { }, mixins: [Tracking.mixin()], props: { - boardsStore: { - type: Object, - required: false, - default: null, - }, canAdminList: { type: Boolean, required: true, @@ -41,9 +36,6 @@ export default { showPage() { this.track('click_button', { label: 'edit_board' }); eventHub.$emit('showBoardModal', formType.edit); - if (this.boardsStore) { - this.boardsStore.showPage(formType.edit); - } }, }, }; diff --git a/app/assets/javascripts/boards/config_toggle.js b/app/assets/javascripts/boards/config_toggle.js index 41938d8e284..945a508c55d 100644 --- a/app/assets/javascripts/boards/config_toggle.js +++ b/app/assets/javascripts/boards/config_toggle.js @@ -2,7 +2,7 @@ import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; import ConfigToggle from './components/config_toggle.vue'; -export default (boardsStore = undefined) => { +export default () => { const el = document.querySelector('.js-board-config'); if (!el) { @@ -15,7 +15,6 @@ export default (boardsStore = undefined) => { render(h) { return h(ConfigToggle, { props: { - boardsStore, canAdminList: parseBoolean(el.dataset.canAdminList), hasScope: parseBoolean(el.dataset.hasScope), }, diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 8e91718f7b3..7862e5685c9 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -298,7 +298,7 @@ export default () => { }); } - boardConfigToggle(boardsStore); + boardConfigToggle(); toggleFocusMode(); toggleLabels(); diff --git a/app/assets/javascripts/content_editor/extensions/audio.js b/app/assets/javascripts/content_editor/extensions/audio.js index 3e746a632df..25d4068c93f 100644 --- a/app/assets/javascripts/content_editor/extensions/audio.js +++ b/app/assets/javascripts/content_editor/extensions/audio.js @@ -1,6 +1,7 @@ import Playable from './playable'; export default Playable.extend({ + name: 'audio', defaultOptions: { ...Playable.options, mediaType: 'audio', diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js index fdc8f173c81..e9d9ec8905a 100644 --- a/app/assets/javascripts/content_editor/extensions/playable.js +++ b/app/assets/javascripts/content_editor/extensions/playable.js @@ -1,9 +1,10 @@ +/* eslint-disable @gitlab/require-i18n-strings */ + import { Node } from '@tiptap/core'; const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType); export default Node.create({ - name: 'playable', group: 'inline', inline: true, draggable: true, @@ -46,7 +47,7 @@ export default Node.create({ parseHTML() { return [ { - tag: '.media-container', + tag: `.${this.options.mediaType}-container`, }, ]; }, @@ -54,7 +55,7 @@ export default Node.create({ renderHTML({ node }) { return [ 'span', - { class: 'media-container' }, + { class: `media-container ${this.options.mediaType}-container` }, [ this.options.mediaType, { diff --git a/app/assets/javascripts/content_editor/extensions/video.js b/app/assets/javascripts/content_editor/extensions/video.js new file mode 100644 index 00000000000..9923b7c04cd --- /dev/null +++ b/app/assets/javascripts/content_editor/extensions/video.js @@ -0,0 +1,10 @@ +import Playable from './playable'; + +export default Playable.extend({ + name: 'video', + defaultOptions: { + ...Playable.options, + mediaType: 'video', + extraElementAttrs: { width: '400' }, + }, +}); diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js index 368730ed926..85191333739 100644 --- a/app/assets/javascripts/content_editor/services/create_content_editor.js +++ b/app/assets/javascripts/content_editor/services/create_content_editor.js @@ -39,6 +39,7 @@ import TableRow from '../extensions/table_row'; import TaskItem from '../extensions/task_item'; import TaskList from '../extensions/task_list'; import Text from '../extensions/text'; +import Video from '../extensions/video'; import { ContentEditor } from './content_editor'; import createMarkdownSerializer from './markdown_serializer'; import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts'; @@ -104,6 +105,7 @@ export const createContentEditor = ({ TaskItem, TaskList, Text, + Video, ]; const allExtensions = [...builtInContentEditorExtensions, ...extensions]; diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index c21482751b1..d208806b247 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -35,6 +35,7 @@ import TableRow from '../extensions/table_row'; import TaskItem from '../extensions/task_item'; import TaskList from '../extensions/task_list'; import Text from '../extensions/text'; +import Video from '../extensions/video'; import { isPlainURL, renderHardBreak, @@ -151,6 +152,7 @@ const defaultSerializerConfig = { else defaultMarkdownSerializer.nodes.bullet_list(state, node); }, [Text.name]: defaultMarkdownSerializer.nodes.text, + [Video.name]: renderPlayable, }, }; diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 8e1fe1e1595..dd8fb1a20b8 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -263,6 +263,8 @@ module ApplicationSettingsHelper :max_attachment_size, :max_import_size, :max_pages_size, + :max_yaml_size_bytes, + :max_yaml_depth, :metrics_method_call_threshold, :minimum_password_length, :mirror_available, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 0402beb6283..869e9ee5bea 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -343,6 +343,8 @@ class ApplicationSetting < ApplicationRecord validates :snippet_size_limit, numericality: { only_integer: true, greater_than: 0 } validates :wiki_page_max_content_bytes, numericality: { only_integer: true, greater_than_or_equal_to: 1.kilobytes } + validates :max_yaml_size_bytes, numericality: { only_integer: true, greater_than: 0 }, presence: true + validates :max_yaml_depth, numericality: { only_integer: true, greater_than: 0 }, presence: true validates :email_restrictions, untrusted_regexp: true diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index 5dfe9f922fd..c194bafeee3 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -109,6 +109,8 @@ module ApplicationSettingImplementation max_artifacts_size: Settings.artifacts['max_size'], max_attachment_size: Settings.gitlab['max_attachment_size'], max_import_size: 0, + max_yaml_size_bytes: 1.megabyte, + max_yaml_depth: 100, minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH, mirror_available: true, notes_create_limit: 300, diff --git a/db/migrate/20210817172214_add_yaml_limits_application_setting.rb b/db/migrate/20210817172214_add_yaml_limits_application_setting.rb new file mode 100644 index 00000000000..f502ef9825b --- /dev/null +++ b/db/migrate/20210817172214_add_yaml_limits_application_setting.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class AddYamlLimitsApplicationSetting < ActiveRecord::Migration[6.1] + DOWNTIME = false + + def change + add_column :application_settings, :max_yaml_size_bytes, :bigint, default: 1.megabyte, null: false + add_column :application_settings, :max_yaml_depth, :integer, default: 100, null: false + end +end diff --git a/db/migrate/20210830154358_add_yaml_limit_constraints.rb b/db/migrate/20210830154358_add_yaml_limit_constraints.rb new file mode 100644 index 00000000000..74236993fff --- /dev/null +++ b/db/migrate/20210830154358_add_yaml_limit_constraints.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# See https://docs.gitlab.com/ee/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddYamlLimitConstraints < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + SIZE_CONSTRAINT_NAME = 'app_settings_yaml_max_size_positive' + DEPTH_CONSTRAINT_NAME = 'app_settings_yaml_max_depth_positive' + + disable_ddl_transaction! + + def up + add_check_constraint :application_settings, 'max_yaml_size_bytes > 0', SIZE_CONSTRAINT_NAME + add_check_constraint :application_settings, 'max_yaml_depth > 0', DEPTH_CONSTRAINT_NAME + end + + def down + remove_check_constraint :application_settings, SIZE_CONSTRAINT_NAME + remove_check_constraint :application_settings, DEPTH_CONSTRAINT_NAME + end +end diff --git a/db/schema_migrations/20210817172214 b/db/schema_migrations/20210817172214 new file mode 100644 index 00000000000..5e334c7d690 --- /dev/null +++ b/db/schema_migrations/20210817172214 @@ -0,0 +1 @@ +d6dd6ce802beeea380e0eb1c564f6a5cbc6d30cb3488a3cb91935e1302a4c387
\ No newline at end of file diff --git a/db/schema_migrations/20210830154358 b/db/schema_migrations/20210830154358 new file mode 100644 index 00000000000..7486c54c4c5 --- /dev/null +++ b/db/schema_migrations/20210830154358 @@ -0,0 +1 @@ +04a44d0e261b26cc7f39b81a4c59ea8e4903d6d7bf73c2004b426204db4491bc
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 957dbb7e644..67438b2dbaf 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9606,6 +9606,8 @@ CREATE TABLE application_settings ( encrypted_customers_dot_jwt_signing_key bytea, encrypted_customers_dot_jwt_signing_key_iv bytea, pypi_package_requests_forwarding boolean DEFAULT true NOT NULL, + max_yaml_size_bytes bigint DEFAULT 1048576 NOT NULL, + max_yaml_depth integer DEFAULT 100 NOT NULL, throttle_unauthenticated_files_api_requests_per_period integer DEFAULT 125 NOT NULL, throttle_unauthenticated_files_api_period_in_seconds integer DEFAULT 15 NOT NULL, throttle_authenticated_files_api_requests_per_period integer DEFAULT 500 NOT NULL, @@ -9615,6 +9617,8 @@ CREATE TABLE application_settings ( CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)), + CONSTRAINT app_settings_yaml_max_depth_positive CHECK ((max_yaml_depth > 0)), + CONSTRAINT app_settings_yaml_max_size_positive CHECK ((max_yaml_size_bytes > 0)), CONSTRAINT check_17d9558205 CHECK ((char_length((kroki_url)::text) <= 1024)), CONSTRAINT check_2dba05b802 CHECK ((char_length(gitpod_url) <= 255)), CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)), diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md index 6fb8d2ed80a..9765e6956ad 100644 --- a/doc/administration/instance_limits.md +++ b/doc/administration/instance_limits.md @@ -513,6 +513,29 @@ Update `ci_jobs_trace_size_limit` with the new value in megabytes: Plan.default.actual_limits.update!(ci_jobs_trace_size_limit: 125) ``` +### Maximum size and depth of CI/CD configuration YAML files + +The default maximum size of a CI/CD configuration YAML file is 1 megabyte and the default depth is 100. + +You can change these limits in the [GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session). +Update `max_yaml_size_bytes` with the new value in megabytes: + +```ruby +ApplicationSetting.update!(max_yaml_size_bytes: 2.megabytes) +``` + +Update `max_yaml_depth` with the new value in megabytes: + +```ruby +ApplicationSetting.update!(max_yaml_depth: 125) +``` + +To disable this limitation entirely, disable the feature flag in the console: + +```ruby +Feature.disable(:ci_yaml_limit_size) +``` + ## Instance monitoring and metrics ### Limit inbound incident management alerts diff --git a/doc/api/discussions.md b/doc/api/discussions.md index 6d15c338f1c..18b74e1450f 100644 --- a/doc/api/discussions.md +++ b/doc/api/discussions.md @@ -965,14 +965,31 @@ Parameters for multiline comments only: #### Line code -A line code is of the form `<SHA>_<old>_<new>`: +A line code is of the form `<SHA>_<old>_<new>`, like this: `adc83b19e793491b1c6ea0fd8b46cd9f32e292fc_5_5` - `<SHA>` is the SHA1 hash of the filename. - `<old>` is the line number before the change. - `<new>` is the line number after the change. -For example, when commenting on an added line number 5, the line code -looks like `adc83b19e793491b1c6ea0fd8b46cd9f32e292fc_5_5`. +For example, if a commit (`<COMMIT_ID>`) deletes line 463 in the README, you can comment +on the deletion by referencing line 463 in the *old* file: + +```shell +curl --request POST --header "PRIVATE-TOKEN: [ACCESS_TOKEN]"\ + --form "note=Very clever to remove this unnecessary line!"\ + --form "path=README" --form "line=463" --form "line_type=old"\ + "https://gitlab.com/api/v4/projects/47/repository/commits/<COMMIT_ID>/comments" +``` + +If a commit (`<COMMIT_ID>`) adds line 157 to `hello.rb`, you can comment on the +addition by referencing line 157 in the *new* file: + +```shell +curl --request POST --header "PRIVATE-TOKEN: [ACCESS_TOKEN]"\ + --form "note=This is brilliant!" --form "path=hello.rb"\ + --form "line=157" --form "line_type=old"\ + "https://gitlab.com/api/v4/projects/47/repository/commits/<COMMIT_ID>/comments" +``` ### Resolve a merge request thread diff --git a/doc/api/runners.md b/doc/api/runners.md index c920de26de5..26de946b382 100644 --- a/doc/api/runners.md +++ b/doc/api/runners.md @@ -673,3 +673,42 @@ Response: |-----------|---------------------------------| | 200 | Credentials are valid | | 403 | Credentials are invalid | + +## Reset instance's runner registration token + +Resets the runner registration token for the GitLab instance. + +```plaintext +POST /runners/reset_registration_token +``` + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/runners/reset_registration_token" +``` + +## Reset project's runner registration token + +Resets the runner registration token for a project. + +```plaintext +POST /projects/:id/runners/reset_registration_token +``` + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/projects/9/runners/reset_registration_token" +``` + +## Reset group's runner registration token + +Resets the runner registration token for a group. + +```plaintext +POST /groups/:id/runners/reset_registration_token +``` + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/9/runners/reset_registration_token" +``` diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md index 216a63108a3..7375e03343a 100644 --- a/doc/user/compliance/license_compliance/index.md +++ b/doc/user/compliance/license_compliance/index.md @@ -53,7 +53,7 @@ You can view and modify existing policies from the [policies](#policies) tab. The following languages and package managers are supported. -Java 8 and Gradle 1.x projects are not supported. The minimum supported version of Maven is 3.2.5. +Gradle 1.x projects are not supported. The minimum supported version of Maven is 3.2.5. | Language | Package managers | Notes | |------------|----------------------------------------------------------------------------------------------|-------| @@ -140,12 +140,12 @@ License Compliance can be configured using CI/CD variables. | `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Pip, Pipenv, Maven, Gradle, Yarn, and npm projects). | | `ASDF_JAVA_VERSION` | no | Version of Java to use for the scan. | | `ASDF_NODEJS_VERSION` | no | Version of Node.js to use for the scan. | -| `ASDF_PYTHON_VERSION` | no | Version of Python to use for the scan. | +| `ASDF_PYTHON_VERSION` | no | Version of Python to use for the scan. [Configuration](#selecting-the-version-of-python) | | `ASDF_RUBY_VERSION` | no | Version of Ruby to use for the scan. | | `GRADLE_CLI_OPTS` | no | Additional arguments for the Gradle executable. If not supplied, defaults to `--exclude-task=test`. | | `LICENSE_FINDER_CLI_OPTS` | no | Additional arguments for the `license_finder` executable. For example, if you have multiple projects in nested directories, you can update your `.gitlab-ci-yml` template to specify a recursive scan, like `LICENSE_FINDER_CLI_OPTS: '--recursive'`. | -| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. | -| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. | +| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. [Configuration](#selecting-the-version-of-java) | +| `LM_PYTHON_VERSION` | no | Version of Python. If set to `3`, dependencies are installed using Python 3 instead of Python 2.7. [Configuration](#selecting-the-version-of-python) | | `MAVEN_CLI_OPTS` | no | Additional arguments for the `mvn` executable. If not supplied, defaults to `-DskipTests`. | | `PIP_INDEX_URL` | no | Base URL of Python Package Index (default: `https://pypi.org/simple/`). | | `SECURE_ANALYZERS_PREFIX` | no | Set the Docker registry base address to download the analyzer from. | @@ -245,6 +245,12 @@ Alternatively, you can use a Java key store to verify the TLS connection. For in generate a key store file, see the [Maven Guide to Remote repository access through authenticated HTTPS](http://maven.apache.org/guides/mini/guide-repository-ssl.html). +### Selecting the version of Java + +License Compliance uses Java 8 by default. You can specify a different Java version using `LM_JAVA_VERSION`. + +`LM_JAVA_VERSION` only accepts versions: 8, 11, 14, 15. + ### Selecting the version of Python > - [Introduced](https://gitlab.com/gitlab-org/security-products/license-management/-/merge_requests/36) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0. @@ -264,6 +270,8 @@ license_scanning: LM_PYTHON_VERSION: 2 ``` +`LM_PYTHON_VERSION` or `ASDF_PYTHON_VERSION` can be used to specify the desired version of Python. When both variables are specified `LM_PYTHON_VERSION` takes precedence. + ### Custom root certificates for Python You can supply a custom root certificate to complete TLS verification by using the diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 19b93aa99ff..b0c2b19416b 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -76,6 +76,21 @@ can assign, categorize, and track progress on a merge request: - [**Notifications**](../../profile/notifications.md): A toggle to select whether or not to receive notifications for updates to a merge request. +## Add changes to a merge request + +If you have permission to add changes to a merge request, you can add your changes +to an existing merge request in several ways, depending on the complexity of your change and whether you need access to a development environment: + +- [Edit changes in the Web IDE](../web_ide/index.md) in your browser. Use this + browser-based method to edit multiple files, or if you are not comfortable with Git commands. + You cannot run tests from the Web IDE. +- [Edit changes in Gitpod](../../../integration/gitpod.md#launch-gitpod-in-gitlab), if you + need a fully-featured environment to both edit files, and run tests afterward. Gitpod + supports running the [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit). + To use Gitpod, you must [enable Gitpod in your user account](../../../integration/gitpod.md#enable-gitpod-in-your-user-settings). +- [Push changes from the command line](../../../gitlab-basics/start-using-git.md), if you are + familiar with Git and the command line. + ## Close a merge request If you decide to permanently stop work on a merge request, diff --git a/doc/user/project/web_ide/img/open_web_ide.png b/doc/user/project/web_ide/img/open_web_ide.png Binary files differdeleted file mode 100644 index 02a5a564472..00000000000 --- a/doc/user/project/web_ide/img/open_web_ide.png +++ /dev/null diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index c2b52f9878f..c5ce9364f0e 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -16,9 +16,22 @@ projects by providing an advanced editor with commit staging. ## Open the Web IDE You can open the Web IDE when viewing a file, from the repository file list, -and from merge requests. - -![Open Web IDE](img/open_web_ide.png) +and from merge requests: + +- *When viewing a file, or the repository file list* - + 1. In the upper right corner of the page, select **Edit in Web IDE** if it is visible. + 1. If **Edit in Web IDE** is not visible: + 1. Select the **(angle-down)** next to **Edit** or **Gitpod**, depending on your configuration. + 1. Select **Edit in Web IDE** from the list to display it as the editing option. + 1. Select **Edit in Web IDE** to open the editor. +- *When viewing a merge request* - + 1. Go to your merge request, and select the **Overview** tab. + 1. Scroll to the widgets area, after the merge request description. + 1. Select **Edit in Web IDE** if it is visible. + 1. If **Edit in Web IDE** is not visible: + 1. Select the **(angle-down)** next to **Open in Gitpod**. + 1. Select **Open in Web IDE** from the list to display it as the editing option. + 1. Select **Open in Web IDE** to open the editor. ## File finder @@ -249,7 +262,7 @@ The image is uploaded to the same directory and is named `image.png` by default. If another file already exists with the same name, a numeric suffix is automatically added to the filename. -There are two ways to preview Markdown content in the Web IDE: +There are two ways to preview Markdown content in the Web IDE: 1. At the top of the file's tab, select **Preview Markdown** to preview the formatting in your file. You can't edit the file in this view. diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index 7f755b1a4d4..93a40925c21 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -222,6 +222,56 @@ module API end end + resource :runners do + before { authenticate_non_get! } + + desc 'Resets runner registration token' do + success Entities::Ci::ResetRegistrationTokenResult + end + post 'reset_registration_token' do + authorize! :update_runners_registration_token + + ApplicationSetting.current.reset_runners_registration_token! + present ApplicationSetting.current_without_cache.runners_registration_token, with: Entities::Ci::ResetRegistrationTokenResult + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + before { authenticate_non_get! } + + desc 'Resets runner registration token' do + success Entities::Ci::ResetRegistrationTokenResult + end + post ':id/runners/reset_registration_token' do + project = find_project! user_project.id + authorize! :update_runners_registration_token, project + + project.reset_runners_token! + present project.runners_token, with: Entities::Ci::ResetRegistrationTokenResult + end + end + + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + before { authenticate_non_get! } + + desc 'Resets runner registration token' do + success Entities::Ci::ResetRegistrationTokenResult + end + post ':id/runners/reset_registration_token' do + group = find_group! user_group.id + authorize! :update_runners_registration_token, group + + group.reset_runners_token! + present group.runners_token, with: Entities::Ci::ResetRegistrationTokenResult + end + end + helpers do def filter_runners(runners, scope, allowed_scopes: ::Ci::Runner::AVAILABLE_SCOPES) return runners unless scope.present? diff --git a/lib/api/entities/ci/reset_registration_token_result.rb b/lib/api/entities/ci/reset_registration_token_result.rb new file mode 100644 index 00000000000..23426432f68 --- /dev/null +++ b/lib/api/entities/ci/reset_registration_token_result.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class ResetRegistrationTokenResult < Grape::Entity + expose(:token) {|object| object} + end + end + end +end diff --git a/lib/gitlab/config/loader/yaml.rb b/lib/gitlab/config/loader/yaml.rb index 80c9abecd8e..f3a3818f010 100644 --- a/lib/gitlab/config/loader/yaml.rb +++ b/lib/gitlab/config/loader/yaml.rb @@ -9,9 +9,6 @@ module Gitlab include Gitlab::Utils::StrongMemoize - MAX_YAML_SIZE = 1.megabyte - MAX_YAML_DEPTH = 100 - def initialize(config, additional_permitted_classes: []) @config = YAML.safe_load(config, permitted_classes: [Symbol, *additional_permitted_classes], @@ -52,8 +49,8 @@ module Gitlab def deep_size strong_memoize(:deep_size) do Gitlab::Utils::DeepSize.new(@config, - max_size: MAX_YAML_SIZE, - max_depth: MAX_YAML_DEPTH) + max_size: Gitlab::CurrentSettings.current_application_settings.max_yaml_size_bytes, + max_depth: Gitlab::CurrentSettings.current_application_settings.max_yaml_depth) end end end diff --git a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb index b9447fd929a..bfcc49885a0 100644 --- a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb @@ -44,7 +44,7 @@ module QA end with_them do - it "pulls an image using the dependency proxy", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1922' do + it "pulls an image using the dependency proxy" do Resource::Repository::Commit.fabricate_via_api! do |commit| commit.project = project commit.commit_message = 'Add .gitlab-ci.yml' diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js index 60d4d1cb8e3..642ffecff2d 100644 --- a/spec/frontend/boards/components/board_settings_sidebar_spec.js +++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js @@ -1,4 +1,3 @@ -import '~/boards/models/list'; import { GlDrawer, GlLabel } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { MountingPortal } from 'portal-vue'; @@ -44,6 +43,7 @@ describe('BoardSettingsSidebar', () => { store, provide: { canAdminList, + scopedLabelsAvailable: false, }, }), ); diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml index 1a6eebc0d4f..feede5458c7 100644 --- a/spec/frontend/fixtures/api_markdown.yml +++ b/spec/frontend/fixtures/api_markdown.yml @@ -173,13 +173,16 @@ Hi @gitlab - thank you for reporting this ~bug (#1) we hope to fix it in %1.1 as part of !1 - name: audio markdown: '![Sample Audio](https://gitlab.com/gitlab.mp3)' -- name: audio_in_lists +- name: video + markdown: '![Sample Video](https://gitlab.com/gitlab.mp4)' +- name: audio_and_video_in_lists markdown: |- * ![Sample Audio](https://gitlab.com/1.mp3) - * ![Sample Audio](https://gitlab.com/2.mp3) + * ![Sample Video](https://gitlab.com/2.mp4) - 1. ![Sample Audio](https://gitlab.com/1.mp3) + 1. ![Sample Video](https://gitlab.com/1.mp4) 2. ![Sample Audio](https://gitlab.com/2.mp3) * [x] ![Sample Audio](https://gitlab.com/1.mp3) * [x] ![Sample Audio](https://gitlab.com/2.mp3) + * [x] ![Sample Video](https://gitlab.com/3.mp4) diff --git a/spec/frontend_integration/fly_out_nav_browser_spec.js b/spec/frontend_integration/fly_out_nav_browser_spec.js new file mode 100644 index 00000000000..ef2afa20528 --- /dev/null +++ b/spec/frontend_integration/fly_out_nav_browser_spec.js @@ -0,0 +1,363 @@ +import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; +import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar'; +import { + calculateTop, + showSubLevelItems, + canShowSubItems, + canShowActiveSubItems, + mouseEnterTopItems, + mouseLeaveTopItem, + getOpenMenu, + setOpenMenu, + mousePos, + getHideSubItemsInterval, + documentMouseMove, + getHeaderHeight, + setSidebar, + subItemsMouseLeave, +} from '~/fly_out_nav'; + +describe('Fly out sidebar navigation', () => { + let el; + let breakpointSize = 'lg'; + + const OLD_SIDEBAR_WIDTH = 200; + const CONTAINER_INITIAL_BOUNDING_RECT = { + x: 8, + y: 8, + width: 769, + height: 0, + top: 8, + right: 777, + bottom: 8, + left: 8, + }; + const SUB_ITEMS_INITIAL_BOUNDING_RECT = { + x: 148, + y: 8, + width: 0, + height: 150, + top: 8, + right: 148, + bottom: 158, + left: 148, + }; + const mockBoundingClientRect = (elem, rect) => { + jest.spyOn(elem, 'getBoundingClientRect').mockReturnValue(rect); + }; + + const findSubItems = () => document.querySelector('.sidebar-sub-level-items'); + const mockBoundingRects = () => { + const subItems = findSubItems(); + mockBoundingClientRect(el, CONTAINER_INITIAL_BOUNDING_RECT); + mockBoundingClientRect(subItems, SUB_ITEMS_INITIAL_BOUNDING_RECT); + }; + const mockSidebarFragment = (styleProps = '') => + `<div class="sidebar-sub-level-items" style="${styleProps}"></div>`; + + beforeEach(() => { + el = document.createElement('div'); + el.style.position = 'relative'; + document.body.appendChild(el); + + jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockImplementation(() => breakpointSize); + }); + + afterEach(() => { + document.body.innerHTML = ''; + breakpointSize = 'lg'; + mousePos.length = 0; + + setSidebar(null); + }); + + describe('calculateTop', () => { + it('returns boundingRect top', () => { + const boundingRect = { + top: 100, + height: 100, + }; + + expect(calculateTop(boundingRect, 100)).toBe(100); + }); + }); + + describe('getHideSubItemsInterval', () => { + beforeEach(() => { + el.innerHTML = mockSidebarFragment('position: fixed; top: 0; left: 100px; height: 150px;'); + mockBoundingRects(); + }); + + it('returns 0 if currentOpenMenu is nil', () => { + setOpenMenu(null); + expect(getHideSubItemsInterval()).toBe(0); + }); + + it('returns 0 if mousePos is empty', () => { + expect(getHideSubItemsInterval()).toBe(0); + }); + + it('returns 0 when mouse above sub-items', () => { + showSubLevelItems(el); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top - 50, + }); + + expect(getHideSubItemsInterval()).toBe(0); + }); + + it('returns 0 when mouse is below sub-items', () => { + const subItems = findSubItems(); + + showSubLevelItems(el); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top - subItems.getBoundingClientRect().height + 50, + }); + + expect(getHideSubItemsInterval()).toBe(0); + }); + + it('returns 300 when mouse is moved towards sub-items', () => { + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + + showSubLevelItems(el); + documentMouseMove({ + clientX: el.getBoundingClientRect().left + 20, + clientY: el.getBoundingClientRect().top + 10, + }); + + expect(getHideSubItemsInterval()).toBe(300); + }); + }); + + describe('mouseLeaveTopItem', () => { + beforeEach(() => { + jest.spyOn(el.classList, 'remove'); + }); + + it('removes is-over class if currentOpenMenu is null', () => { + setOpenMenu(null); + + mouseLeaveTopItem(el); + + expect(el.classList.remove).toHaveBeenCalledWith('is-over'); + }); + + it('removes is-over class if currentOpenMenu is null & there are sub-items', () => { + setOpenMenu(null); + el.innerHTML = mockSidebarFragment('position: absolute'); + + mouseLeaveTopItem(el); + + expect(el.classList.remove).toHaveBeenCalledWith('is-over'); + }); + + it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => { + setOpenMenu(null); + el.innerHTML = mockSidebarFragment('position: absolute'); + + setOpenMenu(findSubItems()); + mouseLeaveTopItem(el); + + expect(el.classList.remove).not.toHaveBeenCalled(); + }); + }); + + describe('mouseEnterTopItems', () => { + beforeEach(() => { + el.innerHTML = mockSidebarFragment( + `position: absolute; top: 0; left: 100px; height: ${OLD_SIDEBAR_WIDTH}px;`, + ); + mockBoundingRects(); + }); + + it('shows sub-items after 0ms if no menu is open', (done) => { + const subItems = findSubItems(); + mouseEnterTopItems(el); + + expect(getHideSubItemsInterval()).toBe(0); + + setTimeout(() => { + expect(subItems.style.display).toBe('block'); + done(); + }); + }); + + it('shows sub-items after 300ms if a menu is currently open', (done) => { + const subItems = findSubItems(); + + documentMouseMove({ + clientX: el.getBoundingClientRect().left, + clientY: el.getBoundingClientRect().top, + }); + + setOpenMenu(subItems); + + documentMouseMove({ + clientX: el.getBoundingClientRect().left + 20, + clientY: el.getBoundingClientRect().top + 10, + }); + + mouseEnterTopItems(el, 0); + + setTimeout(() => { + expect(subItems.style.display).toBe('block'); + + done(); + }); + }); + }); + + describe('showSubLevelItems', () => { + beforeEach(() => { + el.innerHTML = mockSidebarFragment('position: absolute'); + }); + + it('adds is-over class to el', () => { + jest.spyOn(el.classList, 'add'); + + showSubLevelItems(el); + + expect(el.classList.add).toHaveBeenCalledWith('is-over'); + }); + + it('does not show sub-items on mobile', () => { + breakpointSize = 'xs'; + + showSubLevelItems(el); + + expect(findSubItems().style.display).not.toBe('block'); + }); + + it('shows sub-items', () => { + showSubLevelItems(el); + + expect(findSubItems().style.display).toBe('block'); + }); + + it('shows collapsed only sub-items if icon only sidebar', () => { + const subItems = findSubItems(); + const sidebar = document.createElement('div'); + sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS); + subItems.classList.add('is-fly-out-only'); + + setSidebar(sidebar); + + showSubLevelItems(el); + + expect(findSubItems().style.display).toBe('block'); + }); + + it('does not show collapsed only sub-items if icon only sidebar', () => { + const subItems = findSubItems(); + subItems.classList.add('is-fly-out-only'); + + showSubLevelItems(el); + + expect(subItems.style.display).not.toBe('block'); + }); + + it('sets transform of sub-items', () => { + const sidebar = document.createElement('div'); + const subItems = findSubItems(); + + sidebar.style.width = `${OLD_SIDEBAR_WIDTH}px`; + + document.body.appendChild(sidebar); + + setSidebar(sidebar); + showSubLevelItems(el); + + expect(subItems.style.transform).toBe( + `translate3d(${OLD_SIDEBAR_WIDTH}px, ${ + Math.floor(el.getBoundingClientRect().top) - getHeaderHeight() + }px, 0)`, + ); + }); + + it('sets is-above when element is above', () => { + const subItems = findSubItems(); + mockBoundingRects(); + + subItems.style.height = `${window.innerHeight + el.offsetHeight}px`; + el.style.top = `${window.innerHeight - el.offsetHeight}px`; + + jest.spyOn(subItems.classList, 'add'); + + showSubLevelItems(el); + + expect(subItems.classList.add).toHaveBeenCalledWith('is-above'); + }); + }); + + describe('canShowSubItems', () => { + it('returns true if on desktop size', () => { + expect(canShowSubItems()).toBeTruthy(); + }); + + it('returns false if on mobile size', () => { + breakpointSize = 'xs'; + + expect(canShowSubItems()).toBeFalsy(); + }); + }); + + describe('canShowActiveSubItems', () => { + it('returns true by default', () => { + expect(canShowActiveSubItems(el)).toBeTruthy(); + }); + + it('returns false when active & expanded sidebar', () => { + const sidebar = document.createElement('div'); + el.classList.add('active'); + + setSidebar(sidebar); + + expect(canShowActiveSubItems(el)).toBeFalsy(); + }); + + it('returns true when active & collapsed sidebar', () => { + const sidebar = document.createElement('div'); + sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS); + el.classList.add('active'); + + setSidebar(sidebar); + + expect(canShowActiveSubItems(el)).toBeTruthy(); + }); + }); + + describe('subItemsMouseLeave', () => { + beforeEach(() => { + el.innerHTML = mockSidebarFragment('position: absolute'); + + setOpenMenu(findSubItems()); + }); + + it('hides subMenu if element is not hovered', () => { + subItemsMouseLeave(el); + + expect(getOpenMenu()).toBeNull(); + }); + + it('does not hide subMenu if element is hovered', () => { + el.classList.add('is-over'); + subItemsMouseLeave(el); + + expect(getOpenMenu()).not.toBeNull(); + }); + }); +}); diff --git a/spec/lib/gitlab/config/loader/yaml_spec.rb b/spec/lib/gitlab/config/loader/yaml_spec.rb index 731ee12d7f4..be568a8e5f9 100644 --- a/spec/lib/gitlab/config/loader/yaml_spec.rb +++ b/spec/lib/gitlab/config/loader/yaml_spec.rb @@ -15,6 +15,24 @@ RSpec.describe Gitlab::Config::Loader::Yaml do YAML end + context 'when max yaml size and depth are set in ApplicationSetting' do + let(:yaml_size) { 2.megabytes } + let(:yaml_depth) { 200 } + + before do + stub_application_setting(max_yaml_size_bytes: yaml_size, max_yaml_depth: yaml_depth) + end + + it 'uses ApplicationSetting values rather than the defaults' do + expect(Gitlab::Utils::DeepSize) + .to receive(:new) + .with(any_args, { max_size: yaml_size, max_depth: yaml_depth }) + .and_call_original + + loader.load! + end + end + context 'when yaml syntax is correct' do let(:yml) { 'image: ruby:2.7' } diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 93b37b455fd..71a609d8a26 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -79,6 +79,10 @@ RSpec.describe ApplicationSetting do it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) } it { is_expected.to validate_presence_of(:max_artifacts_size) } it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) } + it { is_expected.to validate_presence_of(:max_yaml_size_bytes) } + it { is_expected.to validate_numericality_of(:max_yaml_size_bytes).only_integer.is_greater_than(0) } + it { is_expected.to validate_presence_of(:max_yaml_depth) } + it { is_expected.to validate_numericality_of(:max_yaml_depth).only_integer.is_greater_than(0) } it { is_expected.to validate_presence_of(:max_pages_size) } it 'ensures max_pages_size is an integer greater than 0 (or equal to 0 to indicate unlimited/maximum)' do is_expected.to validate_numericality_of(:max_pages_size).only_integer.is_greater_than_or_equal_to(0) diff --git a/spec/requests/api/ci/runners_reset_registration_token_spec.rb b/spec/requests/api/ci/runners_reset_registration_token_spec.rb new file mode 100644 index 00000000000..7623d3f1b17 --- /dev/null +++ b/spec/requests/api/ci/runners_reset_registration_token_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::Ci::Runners do + subject { post api("#{prefix}/runners/reset_registration_token", user) } + + shared_examples 'bad request' do |result| + it 'returns 400 error' do + expect { subject }.not_to change { get_token } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response).to eq(result) + end + end + + shared_examples 'unauthenticated' do + it 'returns 401 error' do + expect { subject }.not_to change { get_token } + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + shared_examples 'unauthorized' do + it 'returns 403 error' do + expect { subject }.not_to change { get_token } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + shared_examples 'not found' do |scope| + it 'returns 404 error' do + expect { subject }.not_to change { get_token } + + expect(response).to have_gitlab_http_status(:not_found) + expect(json_response).to eq({ 'message' => "404 #{scope.capitalize} Not Found" }) + end + end + + shared_context 'when unauthorized' do |scope| + context 'when unauthorized' do + let_it_be(:user) { create(:user) } + + context "when not a #{scope} member" do + it_behaves_like 'not found', scope + end + + context "with a non-admin #{scope} member" do + before do + target.add_developer(user) + end + + it_behaves_like 'unauthorized' + end + end + end + + shared_context 'when authorized' do |scope| + it 'resets runner registration token' do + expect { subject }.to change { get_token } + + expect(response).to have_gitlab_http_status(:success) + expect(json_response).to eq({ 'token' => get_token }) + end + + if scope != 'instance' + context 'when malformed id is provided' do + let(:prefix) { "/#{scope.pluralize}/some%20string" } + + it_behaves_like 'not found', scope + end + end + end + + describe '/api/v4/runners/reset_registration_token' do + describe 'POST /api/v4/runners/reset_registration_token' do + before do + ApplicationSetting.create_from_defaults + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + end + + let(:prefix) { '' } + + context 'when unauthenticated' do + let(:user) { nil } + + it_behaves_like 'unauthenticated' + end + + context 'when unauthorized' do + let(:user) { create(:user) } + + context "with a non-admin instance member" do + it_behaves_like 'unauthorized' + end + end + + include_context 'when authorized', 'instance' do + let_it_be(:user) { create(:user, :admin) } + + def get_token + ApplicationSetting.current_without_cache.runners_registration_token + end + end + end + end + + describe '/api/v4/groups/:id/runners/reset_registration_token' do + describe 'POST /api/v4/groups/:id/runners/reset_registration_token' do + let_it_be(:group) { create_default(:group, :private) } + + let(:prefix) { "/groups/#{group.id}" } + + include_context 'when unauthorized', 'group' do + let(:target) { group } + end + + include_context 'when authorized', 'group' do + let_it_be(:user) { create_default(:group_member, :maintainer, user: create(:user), group: group ).user } + + def get_token + group.reload.runners_token + end + end + end + end + + describe '/api/v4/projects/:id/runners/reset_registration_token' do + describe 'POST /api/v4/projects/:id/runners/reset_registration_token' do + let_it_be(:project) { create_default(:project) } + + let(:prefix) { "/projects/#{project.id}" } + + include_context 'when unauthorized', 'project' do + let(:target) { project } + end + + include_context 'when authorized', 'project' do + let_it_be(:user) { project.owner } + + def get_token + project.reload.runners_token + end + end + end + end +end |