diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-15 00:10:07 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-12-15 00:10:07 +0000 |
commit | f68b31bd2c8812f4aa3654c6ab7271578c2ceea4 (patch) | |
tree | 7c62f11462cde7d6b0b14d8cbbfe93336f7a25c8 | |
parent | 3a51d1d11d8282ec011f1a79fa10b1ce370e9933 (diff) | |
download | gitlab-ce-f68b31bd2c8812f4aa3654c6ab7271578c2ceea4.tar.gz |
Add latest changes from gitlab-org/gitlab@master
47 files changed, 883 insertions, 139 deletions
@@ -160,7 +160,7 @@ gem 'asciidoctor', '~> 2.0.10' gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-plantuml', '~> 0.0.12' gem 'asciidoctor-kroki', '~> 0.2.2', require: false -gem 'rouge', '~> 3.25.0' +gem 'rouge', '~> 3.26.0' gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 4.2.0' gem 'nokogiri', '~> 1.10.9' diff --git a/Gemfile.lock b/Gemfile.lock index 3c61f223fa9..939c9d8b17b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -986,7 +986,7 @@ GEM rexml (3.2.4) rinku (2.0.0) rotp (2.1.2) - rouge (3.25.0) + rouge (3.26.0) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -1481,7 +1481,7 @@ DEPENDENCIES request_store (~> 1.5) responders (~> 3.0) retriable (~> 3.1.2) - rouge (~> 3.25.0) + rouge (~> 3.26.0) rqrcode-rails3 (~> 0.1.7) rspec-parameterized rspec-rails (~> 4.0.0) diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index e6b0a6fc1c5..c8a31d96a69 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -5,7 +5,7 @@ import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants'; import TemplateSelectorMediator from '../blob/file_template_mediator'; import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown'; import EditorLite from '~/editor/editor_lite'; -import FileTemplateExtension from '~/editor/editor_file_template_ext'; +import { FileTemplateExtension } from '~/editor/editor_file_template_ext'; export default class EditBlob { // The options object has: @@ -16,11 +16,11 @@ export default class EditBlob { if (this.options.isMarkdown) { import('~/editor/editor_markdown_ext') - .then(MarkdownExtension => { - this.editor.use(MarkdownExtension.default); + .then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => { + this.editor.use(new MarkdownExtension()); addEditorMarkdownListeners(this.editor); }) - .catch(() => createFlash(BLOB_EDITOR_ERROR)); + .catch(e => createFlash(`${BLOB_EDITOR_ERROR}: ${e}`)); } this.initModePanesAndLinks(); @@ -42,7 +42,7 @@ export default class EditBlob { blobPath: fileNameEl.value, blobContent: editorEl.innerText, }); - this.editor.use(FileTemplateExtension); + this.editor.use(new FileTemplateExtension()); fileNameEl.addEventListener('change', () => { this.editor.updateModelLanguage(fileNameEl.value); diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js index b02eb37206a..d6f87872bde 100644 --- a/app/assets/javascripts/editor/constants.js +++ b/app/assets/javascripts/editor/constants.js @@ -6,3 +6,7 @@ export const EDITOR_LITE_INSTANCE_ERROR_NO_EL = __( export const URI_PREFIX = 'gitlab'; export const CONTENT_UPDATE_DEBOUNCE = 250; + +export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __( + 'Editor Lite instance is required to set up an extension.', +); diff --git a/app/assets/javascripts/editor/editor_file_template_ext.js b/app/assets/javascripts/editor/editor_file_template_ext.js index 343908b831d..f5474318447 100644 --- a/app/assets/javascripts/editor/editor_file_template_ext.js +++ b/app/assets/javascripts/editor/editor_file_template_ext.js @@ -1,7 +1,8 @@ import { Position } from 'monaco-editor'; +import { EditorLiteExtension } from './editor_lite_extension_base'; -export default { +export class FileTemplateExtension extends EditorLiteExtension { navigateFileStart() { this.setPosition(new Position(1, 1)); - }, -}; + } +} diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/editor_lite.js index e7535c211db..2bd1cdc84d0 100644 --- a/app/assets/javascripts/editor/editor_lite.js +++ b/app/assets/javascripts/editor/editor_lite.js @@ -8,7 +8,7 @@ import { clearDomElement } from './utils'; import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from './constants'; import { uuids } from '~/diffs/utils/uuids'; -export default class Editor { +export default class EditorLite { constructor(options = {}) { this.instances = []; this.options = { @@ -17,7 +17,7 @@ export default class Editor { ...options, }; - Editor.setupMonacoTheme(); + EditorLite.setupMonacoTheme(); registerLanguages(...languages); } @@ -54,12 +54,25 @@ export default class Editor { extensionsArray.forEach(ext => { const prefix = ext.includes('/') ? '' : 'editor/'; const trimmedExt = ext.replace(/^\//, '').trim(); - Editor.pushToImportsArray(promises, `~/${prefix}${trimmedExt}`); + EditorLite.pushToImportsArray(promises, `~/${prefix}${trimmedExt}`); }); return Promise.all(promises); } + static mixIntoInstance(source, inst) { + if (!inst) { + return; + } + const isClassInstance = source.constructor.prototype !== Object.prototype; + const sanitizedSource = isClassInstance ? source.constructor.prototype : source; + Object.getOwnPropertyNames(sanitizedSource).forEach(prop => { + if (prop !== 'constructor') { + Object.assign(inst, { [prop]: source[prop] }); + } + }); + } + /** * Creates a monaco instance with the given options. * @@ -101,10 +114,10 @@ export default class Editor { this.instances.splice(index, 1); model.dispose(); }); - instance.updateModelLanguage = path => Editor.updateModelLanguage(path, instance); + instance.updateModelLanguage = path => EditorLite.updateModelLanguage(path, instance); instance.use = args => this.use(args, instance); - Editor.loadExtensions(extensions, instance) + EditorLite.loadExtensions(extensions, instance) .then(modules => { if (modules) { modules.forEach(module => { @@ -129,10 +142,17 @@ export default class Editor { use(exts = [], instance = null) { const extensions = Array.isArray(exts) ? exts : [exts]; + const initExtensions = inst => { + extensions.forEach(extension => { + EditorLite.mixIntoInstance(extension, inst); + }); + }; if (instance) { - Object.assign(instance, ...extensions); + initExtensions(instance); } else { - this.instances.forEach(inst => Object.assign(inst, ...extensions)); + this.instances.forEach(inst => { + initExtensions(inst); + }); } } } diff --git a/app/assets/javascripts/editor/editor_lite_extension_base.js b/app/assets/javascripts/editor/editor_lite_extension_base.js new file mode 100644 index 00000000000..b8d87fa4969 --- /dev/null +++ b/app/assets/javascripts/editor/editor_lite_extension_base.js @@ -0,0 +1,11 @@ +import { ERROR_INSTANCE_REQUIRED_FOR_EXTENSION } from './constants'; + +export class EditorLiteExtension { + constructor({ instance, ...options } = {}) { + if (instance) { + Object.assign(instance, options); + } else if (Object.entries(options).length) { + throw new Error(ERROR_INSTANCE_REQUIRED_FOR_EXTENSION); + } + } +} diff --git a/app/assets/javascripts/editor/editor_markdown_ext.js b/app/assets/javascripts/editor/editor_markdown_ext.js index c46f5736912..19e0037c175 100644 --- a/app/assets/javascripts/editor/editor_markdown_ext.js +++ b/app/assets/javascripts/editor/editor_markdown_ext.js @@ -1,4 +1,6 @@ -export default { +import { EditorLiteExtension } from './editor_lite_extension_base'; + +export class EditorMarkdownExtension extends EditorLiteExtension { getSelectedText(selection = this.getSelection()) { const { startLineNumber, endLineNumber, startColumn, endColumn } = selection; const valArray = this.getValue().split('\n'); @@ -18,19 +20,19 @@ export default { : [startLineText, endLineText].join('\n'); } return text; - }, + } replaceSelectedText(text, select = undefined) { const forceMoveMarkers = !select; this.executeEdits('', [{ range: this.getSelection(), text, forceMoveMarkers }]); - }, + } moveCursor(dx = 0, dy = 0) { const pos = this.getPosition(); pos.column += dx; pos.lineNumber += dy; this.setPosition(pos); - }, + } /** * Adjust existing selection to select text within the original selection. @@ -91,5 +93,5 @@ export default { .setEndPosition(newEndLineNumber, newEndColumn); this.setSelection(newSelection); - }, -}; + } +} diff --git a/app/finders/ci/daily_build_group_report_results_finder.rb b/app/finders/ci/daily_build_group_report_results_finder.rb index ec41d9d2c45..2f6e0e47017 100644 --- a/app/finders/ci/daily_build_group_report_results_finder.rb +++ b/app/finders/ci/daily_build_group_report_results_finder.rb @@ -4,7 +4,7 @@ module Ci class DailyBuildGroupReportResultsFinder include Gitlab::Allowable - def initialize(current_user:, project:, ref_path:, start_date:, end_date:, limit: nil) + def initialize(current_user:, project:, ref_path: nil, start_date:, end_date:, limit: nil) @current_user = current_user @project = project @ref_path = ref_path @@ -35,11 +35,18 @@ module Ci end def query_params - { + params = { project_id: project, - ref_path: ref_path, date: start_date..end_date } + + if ref_path + params[:ref_path] = ref_path + else + params[:default_branch] = true + end + + params end def none diff --git a/app/graphql/types/ci/ci_cd_setting_type.rb b/app/graphql/types/ci/ci_cd_setting_type.rb new file mode 100644 index 00000000000..207c37f9538 --- /dev/null +++ b/app/graphql/types/ci/ci_cd_setting_type.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Types + module Ci + class CiCdSettingType < BaseObject + graphql_name 'ProjectCiCdSetting' + + authorize :admin_project + + field :merge_pipelines_enabled, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Whether merge pipelines are enabled.', + method: :merge_pipelines_enabled? + field :merge_trains_enabled, GraphQL::BOOLEAN_TYPE, null: true, + description: 'Whether merge trains are enabled.', + method: :merge_trains_enabled? + field :project, Types::ProjectType, null: true, + description: 'Project the CI/CD settings belong to.' + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 2061f91e89a..a7d9548610e 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -191,6 +191,11 @@ module Types description: 'Build pipeline of the project', resolver: Resolvers::ProjectPipelineResolver + field :ci_cd_settings, + Types::Ci::CiCdSettingType, + null: true, + description: 'CI/CD settings for the project' + field :sentry_detailed_error, Types::ErrorTracking::SentryDetailedErrorType, null: true, diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 24809141570..6f063c607b3 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -358,6 +358,7 @@ class MergeRequestDiff < ApplicationRecord if comparison comparison.diffs_in_batch(batch_page, batch_size, diff_options: diff_options) else + reorder_diff_files! diffs_in_batch_collection(batch_page, batch_size, diff_options: diff_options) end end @@ -371,6 +372,7 @@ class MergeRequestDiff < ApplicationRecord if comparison comparison.diffs(diff_options) else + reorder_diff_files! diffs_collection(diff_options) end end @@ -565,7 +567,7 @@ class MergeRequestDiff < ApplicationRecord end def build_merge_request_diff_files(diffs) - diffs.map.with_index do |diff, index| + sort_diffs(diffs).map.with_index do |diff, index| diff_hash = diff.to_hash.merge( binary: false, merge_request_diff_id: self.id, @@ -678,6 +680,7 @@ class MergeRequestDiff < ApplicationRecord rows = build_merge_request_diff_files(diff_collection) create_merge_request_diff_files(rows) + new_attributes[:sorted] = true self.class.uncached { merge_request_diff_files.reset } end @@ -719,6 +722,59 @@ class MergeRequestDiff < ApplicationRecord repo.keep_around(start_commit_sha, head_commit_sha, base_commit_sha) end end + + def reorder_diff_files! + return unless sort_diffs? + return if sorted? || merge_request_diff_files.empty? + + diff_files = sort_diffs(merge_request_diff_files) + + diff_files.each_with_index do |diff_file, index| + diff_file.relative_order = index + end + + transaction do + # The `merge_request_diff_files` table doesn't have an `id` column so + # we cannot use `Gitlab::Database::BulkUpdate`. + MergeRequestDiffFile.where(merge_request_diff_id: id).delete_all + MergeRequestDiffFile.bulk_insert!(diff_files) + update_column(:sorted, true) + end + end + + def sort_diffs(diffs) + return diffs unless sort_diffs? + + diffs.sort do |a, b| + compare_path_parts(path_parts(a), path_parts(b)) + end + end + + def path_parts(diff) + (diff.new_path.presence || diff.old_path).split(::File::SEPARATOR) + end + + # Used for sorting the file paths by: + # 1. Directory name + # 2. Depth + # 3. File name + def compare_path_parts(a_parts, b_parts) + a_part = a_parts.shift + b_part = b_parts.shift + + return 1 if a_parts.size < b_parts.size && a_parts.empty? + return -1 if a_parts.size > b_parts.size && b_parts.empty? + + comparison = a_part <=> b_part + + return comparison unless comparison == 0 + + compare_path_parts(a_parts, b_parts) + end + + def sort_diffs? + Feature.enabled?(:sort_diffs, project, default_enabled: false) + end end MergeRequestDiff.prepend_if_ee('EE::MergeRequestDiff') diff --git a/app/policies/project_ci_cd_setting_policy.rb b/app/policies/project_ci_cd_setting_policy.rb new file mode 100644 index 00000000000..a22b790415b --- /dev/null +++ b/app/policies/project_ci_cd_setting_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ProjectCiCdSettingPolicy < BasePolicy + delegate { @subject.project } +end diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb index 5036f28184c..1409f023f21 100644 --- a/app/serializers/diff_file_base_entity.rb +++ b/app/serializers/diff_file_base_entity.rb @@ -118,7 +118,7 @@ class DiffFileBaseEntity < Grape::Entity strong_memoize(:submodule_links) do next unless diff_file.submodule? - options[:submodule_links].for(diff_file.blob, diff_file.content_sha, diff_file) + options[:submodule_links]&.for(diff_file.blob, diff_file.content_sha, diff_file) end end diff --git a/changelogs/unreleased/26552-sort-diff-files.yml b/changelogs/unreleased/26552-sort-diff-files.yml new file mode 100644 index 00000000000..0bda2489f7c --- /dev/null +++ b/changelogs/unreleased/26552-sort-diff-files.yml @@ -0,0 +1,5 @@ +--- +title: Sort merge request diff files directory first +merge_request: 49118 +author: +type: changed diff --git a/changelogs/unreleased/282520-follow-up-enable-and-disable-merge-train-checkbox-based-on-pipelin.yml b/changelogs/unreleased/282520-follow-up-enable-and-disable-merge-train-checkbox-based-on-pipelin.yml new file mode 100644 index 00000000000..cb2e347e9a9 --- /dev/null +++ b/changelogs/unreleased/282520-follow-up-enable-and-disable-merge-train-checkbox-based-on-pipelin.yml @@ -0,0 +1,5 @@ +--- +title: Add Merge Train Setting to the graphql api +merge_request: 49402 +author: +type: changed diff --git a/changelogs/unreleased/288312-editor-lite-extension-options.yml b/changelogs/unreleased/288312-editor-lite-extension-options.yml new file mode 100644 index 00000000000..4d05f9d29a4 --- /dev/null +++ b/changelogs/unreleased/288312-editor-lite-extension-options.yml @@ -0,0 +1,5 @@ +--- +title: Support extensions as configurable ES6 classes in Editor Lite +merge_request: 49813 +author: +type: added diff --git a/config/feature_flags/development/sort_diffs.yml b/config/feature_flags/development/sort_diffs.yml new file mode 100644 index 00000000000..505b5f0e0b5 --- /dev/null +++ b/config/feature_flags/development/sort_diffs.yml @@ -0,0 +1,8 @@ +--- +name: sort_diffs +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49118 +rollout_issue_url: +milestone: '13.7' +type: development +group: group::code review +default_enabled: false diff --git a/db/migrate/20201202133606_add_sorted_to_merge_request_diffs.rb b/db/migrate/20201202133606_add_sorted_to_merge_request_diffs.rb new file mode 100644 index 00000000000..4c0d28d70a1 --- /dev/null +++ b/db/migrate/20201202133606_add_sorted_to_merge_request_diffs.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddSortedToMergeRequestDiffs < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + add_column :merge_request_diffs, :sorted, :boolean, null: false, default: false + end + end + + def down + with_lock_retries do + remove_column :merge_request_diffs, :sorted + end + end +end diff --git a/db/schema_migrations/20201202133606 b/db/schema_migrations/20201202133606 new file mode 100644 index 00000000000..693c4fc074e --- /dev/null +++ b/db/schema_migrations/20201202133606 @@ -0,0 +1 @@ +83c7e30abb0c8f4e11faa648a4a509029aafa3230e64fe7b14d63f0a39df05ad
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index cfe0936679c..d385987188f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13847,6 +13847,7 @@ CREATE TABLE merge_request_diffs ( external_diff_store integer DEFAULT 1, stored_externally boolean, files_count smallint, + sorted boolean DEFAULT false NOT NULL, CONSTRAINT check_93ee616ac9 CHECK ((external_diff_store IS NOT NULL)) ); diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index d86708e11fd..f065d985f0a 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -16230,6 +16230,11 @@ type Project { ): BoardConnection """ + CI/CD settings for the project + """ + ciCdSettings: ProjectCiCdSetting + + """ Find a single cluster agent by name """ clusterAgent( @@ -17906,6 +17911,23 @@ type Project { wikiEnabled: Boolean } +type ProjectCiCdSetting { + """ + Whether merge pipelines are enabled. + """ + mergePipelinesEnabled: Boolean + + """ + Whether merge trains are enabled. + """ + mergeTrainsEnabled: Boolean + + """ + Project the CI/CD settings belong to. + """ + project: Project +} + """ The connection type for Project. """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 9205dccd6ab..b5ff3a42707 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -48282,6 +48282,20 @@ "deprecationReason": null }, { + "name": "ciCdSettings", + "description": "CI/CD settings for the project", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "ProjectCiCdSetting", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "clusterAgent", "description": "Find a single cluster agent by name", "args": [ @@ -52339,6 +52353,61 @@ }, { "kind": "OBJECT", + "name": "ProjectCiCdSetting", + "description": null, + "fields": [ + { + "name": "mergePipelinesEnabled", + "description": "Whether merge pipelines are enabled.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mergeTrainsEnabled", + "description": "Whether merge trains are enabled.", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "project", + "description": "Project the CI/CD settings belong to.", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "Project", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "ProjectConnection", "description": "The connection type for Project.", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 556ded84621..cba68ceac3b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2521,6 +2521,7 @@ Autogenerated return type of PipelineRetry. | `avatarUrl` | String | URL to avatar image file of the project | | `board` | Board | A single board of the project | | `boards` | BoardConnection | Boards of the project | +| `ciCdSettings` | ProjectCiCdSetting | CI/CD settings for the project | | `clusterAgent` | ClusterAgent | Find a single cluster agent by name | | `clusterAgents` | ClusterAgentConnection | Cluster agents associated with the project | | `codeCoverageSummary` | CodeCoverageSummary | Code coverage summary associated with the project | @@ -2615,6 +2616,14 @@ Autogenerated return type of PipelineRetry. | `webUrl` | String | Web URL of the project | | `wikiEnabled` | Boolean | Indicates if Wikis are enabled for the current user | +### ProjectCiCdSetting + +| Field | Type | Description | +| ----- | ---- | ----------- | +| `mergePipelinesEnabled` | Boolean | Whether merge pipelines are enabled. | +| `mergeTrainsEnabled` | Boolean | Whether merge trains are enabled. | +| `project` | Project | Project the CI/CD settings belong to. | + ### ProjectMember Represents a Project Membership. diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md index 0c1787e9818..787ee8f8573 100644 --- a/doc/ci/pipelines/job_artifacts.md +++ b/doc/ci/pipelines/job_artifacts.md @@ -213,6 +213,17 @@ as artifacts. The collected DAST report uploads to GitLab as an artifact and is summarized in merge requests and the pipeline view. It's also used to provide data for security dashboards. +#### `artifacts:reports:api_fuzzing` **(ULTIMATE)** + +> - Introduced in GitLab 13.4. +> - Requires GitLab Runner 13.4 or later. + +The `api_fuzzing` report collects [API Fuzzing bugs](../../user/application_security/api_fuzzing/index.md) +as artifacts. + +The collected API Fuzzing report uploads to GitLab as an artifact and is summarized in merge +requests and the pipeline view. It's also used to provide data for security dashboards. + #### `artifacts:reports:coverage_fuzzing` **(ULTIMATE)** > - Introduced in GitLab 13.4. diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index b215aecb92d..7a94e03ac5a 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -1594,8 +1594,9 @@ elements: ## GitLab versions -To help users be aware of recent product improvements or additions, we add -GitLab version information to our documentation. +GitLab product documentation pages (not including [Contributor and Development](../../README.md) +pages in the `/development` directory) can include version information to help +users be aware of recent improvements or additions. The GitLab Technical Writing team determines which versions of documentation to display on this site based on the GitLab diff --git a/doc/user/clusters/agent/repository.md b/doc/user/clusters/agent/repository.md index 6f2e1bd9bbf..b71bbc29ef9 100644 --- a/doc/user/clusters/agent/repository.md +++ b/doc/user/clusters/agent/repository.md @@ -40,9 +40,10 @@ using the Agent. To use multiple YAML files, specify a `paths` attribute in the `gitops` section. -By default, the Agent monitors all types of resources. You can exclude some types of resources -from monitoring. This enables you to reduce the permissions needed by the GitOps feature, -through `resource_exclusions`. +By default, the Agent monitors all +[Kubernetes object types](https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#required-fields). +You can exclude some types of resources from monitoring. This enables you to reduce +the permissions needed by the GitOps feature, through `resource_exclusions`. To enable a specific named resource, first use `resource_inclusions` to enable desired resources. The following file excerpt includes specific `api_groups` and `kinds`. The `resource_exclusions` @@ -60,6 +61,8 @@ gitops: # Holds the only API groups and kinds of resources that gitops will monitor. # Inclusion rules are evaluated first, then exclusion rules. # If there is still no match, resource is monitored. + # Resources: https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/#required-fields + # Groups: https://kubernetes.io/docs/concepts/overview/kubernetes-api/#api-groups-and-versioning resource_inclusions: - api_groups: - apps diff --git a/doc/user/group/repositories_analytics/index.md b/doc/user/group/repositories_analytics/index.md index 3ba158cf6a1..fc4fb0236de 100644 --- a/doc/user/group/repositories_analytics/index.md +++ b/doc/user/group/repositories_analytics/index.md @@ -25,7 +25,9 @@ To see the latest code coverage for each project in your group: > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/215104) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4. -You can get a CSV of the code coverage data for all of the projects in your group. This report has a maximum of 1000 records. To get the report: +You can get a CSV of the code coverage data for all of the projects in your group. This report has a maximum of 1000 records. The code coverage data is from the default branch in each project. + +To get the report: 1. Go to your group's **Analytics > Repositories** page 1. Click **Download historic test coverage data (.csv)**, @@ -44,6 +46,9 @@ For each day that a coverage report was generated by a job in a project's pipeli If the project's code coverage was calculated more than once in a day, we will take the last value from that day. +NOTE: +[In GitLab 13.7 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/270102), group code coverage data is taken from the configured [default branch](../../project/repository/branches/index.md#default-branch). In earlier versions, it is taken from the `master` branch. + <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md index d7586227ace..42729776fe1 100644 --- a/doc/user/packages/dependency_proxy/index.md +++ b/doc/user/packages/dependency_proxy/index.md @@ -17,6 +17,11 @@ upstream images. In the case of CI/CD, the Dependency Proxy receives a request and returns the upstream image from a registry, acting as a pull-through cache. +NOTE: +The Dependency Proxy is not compatible with Docker version 20.x and later. +If you are using the Dependency Proxy, Docker version 19.x.x is recommended until +[issue #290944](https://gitlab.com/gitlab-org/gitlab/-/issues/290944) is resolved. + ## Prerequisites The Dependency Proxy must be [enabled by an administrator](../../../administration/packages/dependency_proxy.md). @@ -89,7 +94,7 @@ You can authenticate using: #### Authenticate within CI/CD -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280582) in 13.7. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/280582) in 13.7. To work with the Dependency Proxy in [GitLab CI/CD](../../../ci/README.md), you can use: diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md index 09c1fa20ac5..cb5ff27da96 100644 --- a/doc/user/project/settings/project_access_tokens.md +++ b/doc/user/project/settings/project_access_tokens.md @@ -13,8 +13,8 @@ Project access tokens are supported for self-managed instances on Core and above > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2587) in GitLab 13.0. > - It was [deployed](https://gitlab.com/groups/gitlab-org/-/epics/2587) behind a feature flag, disabled by default. > - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/218722) in GitLab 13.3. -> - [Became available on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in 13.5. > - It's recommended for production use. +> - [Became available on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in 13.5 for paid groups only. WARNING: This feature might not be available to you. Check the **version history** note above for details. diff --git a/lib/gitlab/auth/otp/strategies/forti_authenticator.rb b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb index 91bac0461a3..c1433f05db2 100644 --- a/lib/gitlab/auth/otp/strategies/forti_authenticator.rb +++ b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb @@ -18,6 +18,9 @@ module Gitlab # Successful authentication results in HTTP 200: OK # https://docs.fortinet.com/document/fortiauthenticator/6.2.0/rest-api-solution-guide/704555/authentication-auth response.ok? ? success : error_from_response(response) + rescue StandardError => ex + Gitlab::AppLogger.error(ex) + error(ex.message) end private @@ -32,7 +35,7 @@ module Gitlab def api_credentials { username: ::Gitlab.config.forti_authenticator.username, - password: ::Gitlab.config.forti_authenticator.token } + password: ::Gitlab.config.forti_authenticator.access_token } end end end diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb index 6090d1b9f69..8df4bc3de05 100644 --- a/lib/gitlab/git/diff_collection.rb +++ b/lib/gitlab/git/diff_collection.rb @@ -66,6 +66,12 @@ module Gitlab @iterator = nil end + def sort(&block) + @array = @array.sort(&block) + + self + end + def empty? any? # Make sure the iterator has been exercised @empty diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f92745e57f7..c8907263de2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10209,6 +10209,9 @@ msgstr "" msgid "Editing" msgstr "" +msgid "Editor Lite instance is required to set up an extension." +msgstr "" + msgid "Elasticsearch AWS IAM credentials" msgstr "" diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb index 2cb2ee7f6f2..0fd140a00bd 100644 --- a/spec/features/merge_request/user_comments_on_diff_spec.rb +++ b/spec/features/merge_request/user_comments_on_diff_spec.rb @@ -31,7 +31,7 @@ RSpec.describe 'User comments on a diff', :js do click_button('Add comment now') end - page.within('.diff-files-holder > div:nth-child(3)') do + page.within('.diff-files-holder > div:nth-child(6)') do expect(page).to have_content('Line is wrong') find('.js-diff-more-actions').click @@ -53,7 +53,7 @@ RSpec.describe 'User comments on a diff', :js do wait_for_requests - page.within('.diff-files-holder > div:nth-child(2) .note-body > .note-text') do + page.within('.diff-files-holder > div:nth-child(5) .note-body > .note-text') do expect(page).to have_content('Line is correct') end @@ -67,7 +67,7 @@ RSpec.describe 'User comments on a diff', :js do wait_for_requests # Hide the comment. - page.within('.diff-files-holder > div:nth-child(3)') do + page.within('.diff-files-holder > div:nth-child(6)') do find('.js-diff-more-actions').click click_button 'Hide comments on this file' @@ -76,22 +76,22 @@ RSpec.describe 'User comments on a diff', :js do # At this moment a user should see only one comment. # The other one should be hidden. - page.within('.diff-files-holder > div:nth-child(2) .note-body > .note-text') do + page.within('.diff-files-holder > div:nth-child(5) .note-body > .note-text') do expect(page).to have_content('Line is correct') end # Show the comment. - page.within('.diff-files-holder > div:nth-child(3)') do + page.within('.diff-files-holder > div:nth-child(6)') do find('.js-diff-more-actions').click click_button 'Show comments on this file' end # Now both the comments should be shown. - page.within('.diff-files-holder > div:nth-child(3) .note-body > .note-text') do + page.within('.diff-files-holder > div:nth-child(6) .note-body > .note-text') do expect(page).to have_content('Line is wrong') end - page.within('.diff-files-holder > div:nth-child(2) .note-body > .note-text') do + page.within('.diff-files-holder > div:nth-child(5) .note-body > .note-text') do expect(page).to have_content('Line is correct') end @@ -102,11 +102,11 @@ RSpec.describe 'User comments on a diff', :js do wait_for_requests - page.within('.diff-files-holder > div:nth-child(3) .parallel .note-body > .note-text') do + page.within('.diff-files-holder > div:nth-child(6) .parallel .note-body > .note-text') do expect(page).to have_content('Line is wrong') end - page.within('.diff-files-holder > div:nth-child(2) .parallel .note-body > .note-text') do + page.within('.diff-files-holder > div:nth-child(5) .parallel .note-body > .note-text') do expect(page).to have_content('Line is correct') end end @@ -204,7 +204,7 @@ RSpec.describe 'User comments on a diff', :js do click_button('Add comment now') end - page.within('.diff-file:nth-of-type(5) .discussion .note') do + page.within('.diff-file:nth-of-type(1) .discussion .note') do find('.js-note-edit').click page.within('.current-note-edit-form') do @@ -215,7 +215,7 @@ RSpec.describe 'User comments on a diff', :js do expect(page).not_to have_button('Save comment', disabled: true) end - page.within('.diff-file:nth-of-type(5) .discussion .note') do + page.within('.diff-file:nth-of-type(1) .discussion .note') do expect(page).to have_content('Typo, please fix').and have_no_content('Line is wrong') end end @@ -234,7 +234,7 @@ RSpec.describe 'User comments on a diff', :js do expect(page).to have_content('1') end - page.within('.diff-file:nth-of-type(5) .discussion .note') do + page.within('.diff-file:nth-of-type(1) .discussion .note') do find('.more-actions').click find('.more-actions .dropdown-menu li', match: :first) diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb index 9556142ecb8..794dfd7c8da 100644 --- a/spec/features/merge_request/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb @@ -74,7 +74,10 @@ RSpec.describe 'Merge request > User posts diff notes', :js do context 'with an unfolded line' do before do - find('.js-unfold', match: :first).click + page.within('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do + find('.js-unfold', match: :first).click + end + wait_for_requests end @@ -137,7 +140,10 @@ RSpec.describe 'Merge request > User posts diff notes', :js do context 'with an unfolded line' do before do - find('.js-unfold', match: :first).click + page.within('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do + find('.js-unfold', match: :first).click + end + wait_for_requests end diff --git a/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb b/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb index bb4bf0864c9..ad9c342df3e 100644 --- a/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb +++ b/spec/features/merge_request/user_views_diffs_file_by_file_spec.rb @@ -23,12 +23,12 @@ RSpec.describe 'User views diffs file-by-file', :js do it 'shows diffs file-by-file' do page.within('#diffs') do expect(page).to have_selector('.file-holder', count: 1) - expect(page).to have_selector('.diff-file .file-title', text: '.DS_Store') + expect(page).to have_selector('.diff-file .file-title', text: 'files/ruby/popen.rb') find('.page-link.next-page-item').click expect(page).to have_selector('.file-holder', count: 1) - expect(page).to have_selector('.diff-file .file-title', text: '.gitignore') + expect(page).to have_selector('.diff-file .file-title', text: 'files/ruby/regex.rb') end end end diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb index e1865fe2e14..a0b3067994c 100644 --- a/spec/features/merge_request/user_views_diffs_spec.rb +++ b/spec/features/merge_request/user_views_diffs_spec.rb @@ -22,8 +22,8 @@ RSpec.describe 'User views diffs', :js do it 'unfolds diffs upwards' do first('.js-unfold').click - page.within('.file-holder[id="a5cc2925ca8258af241be7e5b0381edf30266302"]') do - expect(find('.text-file')).to have_content('.bundle') + page.within('.file-holder[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd"]') do + expect(find('.text-file')).to have_content('fileutils') expect(page).to have_selector('.new_line [data-linenumber="1"]', count: 1) end end diff --git a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb index c0434b5f371..28a732fda82 100644 --- a/spec/finders/ci/daily_build_group_report_results_finder_spec.rb +++ b/spec/finders/ci/daily_build_group_report_results_finder_spec.rb @@ -4,57 +4,77 @@ require 'spec_helper' RSpec.describe Ci::DailyBuildGroupReportResultsFinder do describe '#execute' do - let(:project) { create(:project, :private) } - let(:ref_path) { 'refs/heads/master' } + let_it_be(:project) { create(:project, :private) } + let_it_be(:current_user) { project.owner } + let_it_be(:ref_path) { 'refs/heads/master' } let(:limit) { nil } + let_it_be(:default_branch) { false } - let!(:rspec_coverage_1) { create_daily_coverage('rspec', 79.0, '2020-03-09') } - let!(:karma_coverage_1) { create_daily_coverage('karma', 89.0, '2020-03-09') } - let!(:rspec_coverage_2) { create_daily_coverage('rspec', 95.0, '2020-03-10') } - let!(:karma_coverage_2) { create_daily_coverage('karma', 92.0, '2020-03-10') } - let!(:rspec_coverage_3) { create_daily_coverage('rspec', 97.0, '2020-03-11') } - let!(:karma_coverage_3) { create_daily_coverage('karma', 99.0, '2020-03-11') } + let_it_be(:rspec_coverage_1) { create_daily_coverage('rspec', 79.0, '2020-03-09') } + let_it_be(:karma_coverage_1) { create_daily_coverage('karma', 89.0, '2020-03-09') } + let_it_be(:rspec_coverage_2) { create_daily_coverage('rspec', 95.0, '2020-03-10') } + let_it_be(:karma_coverage_2) { create_daily_coverage('karma', 92.0, '2020-03-10') } + let_it_be(:rspec_coverage_3) { create_daily_coverage('rspec', 97.0, '2020-03-11') } + let_it_be(:karma_coverage_3) { create_daily_coverage('karma', 99.0, '2020-03-11') } - subject do - described_class.new( + let(:attributes) do + { current_user: current_user, project: project, ref_path: ref_path, start_date: '2020-03-09', end_date: '2020-03-10', limit: limit - ).execute + } end - context 'when current user is allowed to read build report results' do - let(:current_user) { project.owner } + subject(:coverages) do + described_class.new(**attributes).execute + end + + context 'when ref_path is present' do + context 'when current user is allowed to read build report results' do + it 'returns all matching results within the given date range' do + expect(coverages).to match_array([ + karma_coverage_2, + rspec_coverage_2, + karma_coverage_1, + rspec_coverage_1 + ]) + end + + context 'and limit is specified' do + let(:limit) { 2 } - it 'returns all matching results within the given date range' do - expect(subject).to match_array([ - karma_coverage_2, - rspec_coverage_2, - karma_coverage_1, - rspec_coverage_1 - ]) + it 'returns limited number of matching results within the given date range' do + expect(coverages).to match_array([ + karma_coverage_2, + rspec_coverage_2 + ]) + end + end end - context 'and limit is specified' do - let(:limit) { 2 } + context 'when current user is not allowed to read build report results' do + let(:current_user) { create(:user) } - it 'returns limited number of matching results within the given date range' do - expect(subject).to match_array([ - karma_coverage_2, - rspec_coverage_2 - ]) + it 'returns an empty result' do + expect(coverages).to be_empty end end end - context 'when current user is not allowed to read build report results' do - let(:current_user) { create(:user) } + context 'when ref_path is not present' do + let(:ref_path) { nil } - it 'returns an empty result' do - expect(subject).to be_empty + context 'when coverages exist for the default branch' do + let(:default_branch) { true } + + it 'returns coverage for the default branch' do + rspec_coverage_4 = create_daily_coverage('rspec', 66.0, '2020-03-10') + + expect(coverages).to contain_exactly(rspec_coverage_4) + end end end end @@ -65,10 +85,11 @@ RSpec.describe Ci::DailyBuildGroupReportResultsFinder do create( :ci_daily_build_group_report_result, project: project, - ref_path: ref_path, + ref_path: ref_path || 'feature-branch', group_name: group_name, data: { 'coverage' => coverage }, - date: date + date: date, + default_branch: default_branch ) end end diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js index ac8b916e448..0ac379e03c9 100644 --- a/spec/frontend/blob_edit/edit_blob_spec.js +++ b/spec/frontend/blob_edit/edit_blob_spec.js @@ -1,11 +1,12 @@ import waitForPromises from 'helpers/wait_for_promises'; import EditBlob from '~/blob_edit/edit_blob'; import EditorLite from '~/editor/editor_lite'; -import MarkdownExtension from '~/editor/editor_markdown_ext'; -import FileTemplateExtension from '~/editor/editor_file_template_ext'; +import { EditorMarkdownExtension } from '~/editor/editor_markdown_ext'; +import { FileTemplateExtension } from '~/editor/editor_file_template_ext'; jest.mock('~/editor/editor_lite'); jest.mock('~/editor/editor_markdown_ext'); +jest.mock('~/editor/editor_file_template_ext'); describe('Blob Editing', () => { const useMock = jest.fn(); @@ -20,6 +21,10 @@ describe('Blob Editing', () => { ); jest.spyOn(EditorLite.prototype, 'createInstance').mockReturnValue(mockInstance); }); + afterEach(() => { + EditorMarkdownExtension.mockClear(); + FileTemplateExtension.mockClear(); + }); const editorInst = isMarkdown => { return new EditBlob({ @@ -34,20 +39,20 @@ describe('Blob Editing', () => { it('loads FileTemplateExtension by default', async () => { await initEditor(); - expect(useMock).toHaveBeenCalledWith(FileTemplateExtension); + expect(FileTemplateExtension).toHaveBeenCalledTimes(1); }); describe('Markdown', () => { it('does not load MarkdownExtension by default', async () => { await initEditor(); - expect(useMock).not.toHaveBeenCalledWith(MarkdownExtension); + expect(EditorMarkdownExtension).not.toHaveBeenCalled(); }); it('loads MarkdownExtension only for the markdown files', async () => { await initEditor(true); expect(useMock).toHaveBeenCalledTimes(2); - expect(useMock).toHaveBeenNthCalledWith(1, FileTemplateExtension); - expect(useMock).toHaveBeenNthCalledWith(2, MarkdownExtension); + expect(FileTemplateExtension).toHaveBeenCalledTimes(1); + expect(EditorMarkdownExtension).toHaveBeenCalledTimes(1); }); }); }); diff --git a/spec/frontend/editor/editor_lite_extension_base_spec.js b/spec/frontend/editor/editor_lite_extension_base_spec.js new file mode 100644 index 00000000000..ff53640b096 --- /dev/null +++ b/spec/frontend/editor/editor_lite_extension_base_spec.js @@ -0,0 +1,44 @@ +import { ERROR_INSTANCE_REQUIRED_FOR_EXTENSION } from '~/editor/constants'; +import { EditorLiteExtension } from '~/editor/editor_lite_extension_base'; + +describe('The basis for an Editor Lite extension', () => { + let ext; + const defaultOptions = { foo: 'bar' }; + + it.each` + description | instance | options + ${'accepts configuration options and instance'} | ${{}} | ${defaultOptions} + ${'leaves instance intact if no options are passed'} | ${{}} | ${undefined} + ${'does not fail if both instance and the options are omitted'} | ${undefined} | ${undefined} + ${'throws if only options are passed'} | ${undefined} | ${defaultOptions} + `('$description', ({ instance, options } = {}) => { + const originalInstance = { ...instance }; + + if (instance) { + if (options) { + Object.entries(options).forEach(prop => { + expect(instance[prop]).toBeUndefined(); + }); + // Both instance and options are passed + ext = new EditorLiteExtension({ instance, ...options }); + Object.entries(options).forEach(([prop, value]) => { + expect(ext[prop]).toBeUndefined(); + expect(instance[prop]).toBe(value); + }); + } else { + ext = new EditorLiteExtension({ instance }); + expect(instance).toEqual(originalInstance); + } + } else if (options) { + // Options are passed without instance + expect(() => { + ext = new EditorLiteExtension({ ...options }); + }).toThrow(ERROR_INSTANCE_REQUIRED_FOR_EXTENSION); + } else { + // Neither options nor instance are passed + expect(() => { + ext = new EditorLiteExtension(); + }).not.toThrow(); + } + }); +}); diff --git a/spec/frontend/editor/editor_lite_spec.js b/spec/frontend/editor/editor_lite_spec.js index 2968984df01..3a7680f6d17 100644 --- a/spec/frontend/editor/editor_lite_spec.js +++ b/spec/frontend/editor/editor_lite_spec.js @@ -1,6 +1,8 @@ +/* eslint-disable max-classes-per-file */ import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor'; import waitForPromises from 'helpers/wait_for_promises'; import Editor from '~/editor/editor_lite'; +import { EditorLiteExtension } from '~/editor/editor_lite_extension_base'; import { DEFAULT_THEME, themes } from '~/ide/lib/themes'; import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from '~/editor/constants'; @@ -242,17 +244,53 @@ describe('Base editor', () => { describe('extensions', () => { let instance; - const foo1 = jest.fn(); - const foo2 = jest.fn(); - const bar = jest.fn(); - const MyExt1 = { - foo: foo1, + const alphaRes = jest.fn(); + const betaRes = jest.fn(); + const fooRes = jest.fn(); + const barRes = jest.fn(); + class AlphaClass { + constructor() { + this.res = alphaRes; + } + alpha() { + return this?.nonExistentProp || alphaRes; + } + } + class BetaClass { + beta() { + return this?.nonExistentProp || betaRes; + } + } + class WithStaticMethod { + constructor({ instance: inst, ...options } = {}) { + Object.assign(inst, options); + } + static computeBoo(a) { + return a + 1; + } + boo() { + return WithStaticMethod.computeBoo(this.base); + } + } + class WithStaticMethodExtended extends EditorLiteExtension { + static computeBoo(a) { + return a + 1; + } + boo() { + return WithStaticMethodExtended.computeBoo(this.base); + } + } + const AlphaExt = new AlphaClass(); + const BetaExt = new BetaClass(); + const FooObjExt = { + foo() { + return fooRes; + }, }; - const MyExt2 = { - bar, - }; - const MyExt3 = { - foo: foo2, + const BarObjExt = { + bar() { + return barRes; + }, }; describe('basic functionality', () => { @@ -260,13 +298,6 @@ describe('Base editor', () => { instance = editor.createInstance({ el: editorEl, blobPath, blobContent }); }); - it('is extensible with the extensions', () => { - expect(instance.foo).toBeUndefined(); - - instance.use(MyExt1); - expect(instance.foo).toEqual(foo1); - }); - it('does not fail if no extensions supplied', () => { const spy = jest.spyOn(global.console, 'error'); instance.use(); @@ -274,24 +305,80 @@ describe('Base editor', () => { expect(spy).not.toHaveBeenCalled(); }); - it('is extensible with multiple extensions', () => { - expect(instance.foo).toBeUndefined(); - expect(instance.bar).toBeUndefined(); + it("does not extend instance with extension's constructor", () => { + expect(instance.constructor).toBeDefined(); + const { constructor } = instance; + + expect(AlphaExt.constructor).toBeDefined(); + expect(AlphaExt.constructor).not.toEqual(constructor); + + instance.use(AlphaExt); + expect(instance.constructor).toBe(constructor); + }); + + it.each` + type | extensions | methods | expectations + ${'ES6 classes'} | ${AlphaExt} | ${['alpha']} | ${[alphaRes]} + ${'multiple ES6 classes'} | ${[AlphaExt, BetaExt]} | ${['alpha', 'beta']} | ${[alphaRes, betaRes]} + ${'simple objects'} | ${FooObjExt} | ${['foo']} | ${[fooRes]} + ${'multiple simple objects'} | ${[FooObjExt, BarObjExt]} | ${['foo', 'bar']} | ${[fooRes, barRes]} + ${'combination of ES6 classes and objects'} | ${[AlphaExt, BarObjExt]} | ${['alpha', 'bar']} | ${[alphaRes, barRes]} + `('is extensible with $type', ({ extensions, methods, expectations } = {}) => { + methods.forEach(method => { + expect(instance[method]).toBeUndefined(); + }); - instance.use([MyExt1, MyExt2]); + instance.use(extensions); - expect(instance.foo).toEqual(foo1); - expect(instance.bar).toEqual(bar); + methods.forEach(method => { + expect(instance[method]).toBeDefined(); + }); + + expectations.forEach((expectation, i) => { + expect(instance[methods[i]].call()).toEqual(expectation); + }); }); + it('does not extend instance with private data of an extension', () => { + const ext = new WithStaticMethod({ instance }); + ext.staticMethod = () => { + return 'foo'; + }; + ext.staticProp = 'bar'; + + expect(instance.boo).toBeUndefined(); + expect(instance.staticMethod).toBeUndefined(); + expect(instance.staticProp).toBeUndefined(); + + instance.use(ext); + + expect(instance.boo).toBeDefined(); + expect(instance.staticMethod).toBeUndefined(); + expect(instance.staticProp).toBeUndefined(); + }); + + it.each([WithStaticMethod, WithStaticMethodExtended])( + 'properly resolves data for an extension with private data', + ExtClass => { + const base = 1; + expect(instance.base).toBeUndefined(); + expect(instance.boo).toBeUndefined(); + + const ext = new ExtClass({ instance, base }); + + instance.use(ext); + expect(instance.base).toBe(1); + expect(instance.boo()).toBe(2); + }, + ); + it('uses the last definition of a method in case of an overlap', () => { - instance.use([MyExt1, MyExt2, MyExt3]); - expect(instance).toEqual( - expect.objectContaining({ - foo: foo2, - bar, - }), - ); + const FooObjExt2 = { foo: 'foo2' }; + instance.use([FooObjExt, BarObjExt, FooObjExt2]); + expect(instance).toMatchObject({ + foo: 'foo2', + ...BarObjExt, + }); }); it('correctly resolves references withing extensions', () => { @@ -396,15 +483,15 @@ describe('Base editor', () => { }); it('extends all instances if no specific instance is passed', () => { - editor.use(MyExt1); - expect(inst1.foo).toEqual(foo1); - expect(inst2.foo).toEqual(foo1); + editor.use(AlphaExt); + expect(inst1.alpha()).toEqual(alphaRes); + expect(inst2.alpha()).toEqual(alphaRes); }); it('extends specific instance if it has been passed', () => { - editor.use(MyExt1, inst2); - expect(inst1.foo).toBeUndefined(); - expect(inst2.foo).toEqual(foo1); + editor.use(AlphaExt, inst2); + expect(inst1.alpha).toBeUndefined(); + expect(inst2.alpha()).toEqual(alphaRes); }); }); }); diff --git a/spec/frontend/editor/editor_markdown_ext_spec.js b/spec/frontend/editor/editor_markdown_ext_spec.js index 30ab29aad35..b432d4d66ad 100644 --- a/spec/frontend/editor/editor_markdown_ext_spec.js +++ b/spec/frontend/editor/editor_markdown_ext_spec.js @@ -1,6 +1,6 @@ import { Range, Position } from 'monaco-editor'; import EditorLite from '~/editor/editor_lite'; -import EditorMarkdownExtension from '~/editor/editor_markdown_ext'; +import { EditorMarkdownExtension } from '~/editor/editor_markdown_ext'; describe('Markdown Extension for Editor Lite', () => { let editor; @@ -31,7 +31,7 @@ describe('Markdown Extension for Editor Lite', () => { blobPath: filePath, blobContent: text, }); - editor.use(EditorMarkdownExtension); + editor.use(new EditorMarkdownExtension()); }); afterEach(() => { diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb index e8e3955affb..88a245b6b10 100644 --- a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb +++ b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb @@ -12,6 +12,7 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do let(:api_token) { 's3cr3t' } let(:forti_authenticator_auth_url) { "https://#{host}:#{port}/api/v1/auth/" } + let(:response_status) { 200 } subject(:validate) { described_class.new(user).validate(otp_code) } @@ -23,20 +24,20 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do host: host, port: port, username: api_username, - token: api_token + access_token: api_token ) request_body = { username: user.username, token_code: otp_code } stub_request(:post, forti_authenticator_auth_url) - .with(body: JSON(request_body), headers: { 'Content-Type' => 'application/json' }) - .to_return(status: response_status, body: '', headers: {}) + .with(body: JSON(request_body), + headers: { 'Content-Type': 'application/json' }, + basic_auth: [api_username, api_token]) + .to_return(status: response_status, body: '') end context 'successful validation' do - let(:response_status) { 200 } - it 'returns success' do expect(validate[:status]).to eq(:success) end @@ -50,6 +51,16 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do end end + context 'unexpected error' do + it 'returns error' do + error_message = 'boom!' + stub_request(:post, forti_authenticator_auth_url).to_raise(StandardError.new(error_message)) + + expect(validate[:status]).to eq(:error) + expect(validate[:message]).to eq(error_message) + end + end + def stub_forti_authenticator_config(forti_authenticator_settings) allow(::Gitlab.config.forti_authenticator).to(receive_messages(forti_authenticator_settings)) end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb index 3cbc280d9ab..959afb98fd1 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb @@ -130,6 +130,7 @@ RSpec.describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do end let(:diffable) { merge_request.merge_request_diff } + let(:batch_page) { 2 } let(:stub_path) { '.gitignore' } subject do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 93f4405fc9c..8f1db484ed1 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -219,6 +219,7 @@ MergeRequestDiff: - start_commit_sha - commits_count - files_count +- sorted MergeRequestDiffCommit: - merge_request_diff_id - relative_order diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 3b21d052a8e..a5493d1650b 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -419,7 +419,7 @@ RSpec.describe MergeRequestDiff do context 'when persisted files available' do it 'returns paginated diffs' do - diffs = diff_with_commits.diffs_in_batch(1, 10, diff_options: {}) + diffs = diff_with_commits.diffs_in_batch(1, 10, diff_options: diff_options) expect(diffs).to be_a(Gitlab::Diff::FileCollection::MergeRequestDiffBatch) expect(diffs.diff_files.size).to eq(10) @@ -427,6 +427,150 @@ RSpec.describe MergeRequestDiff do next_page: 2, total_pages: 2) end + + it 'sorts diff files directory first' do + diff_with_commits.update!(sorted: false) # Mark as unsorted so it'll re-order + + expect(diff_with_commits.diffs_in_batch(1, 10, diff_options: diff_options).diff_file_paths).to eq([ + 'bar/branch-test.txt', + 'custom-highlighting/test.gitlab-custom', + 'encoding/iso8859.txt', + 'files/images/wm.svg', + 'files/js/commit.coffee', + 'files/lfs/lfs_object.iso', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/.DS_Store', + 'files/whitespace' + ]) + end + + context 'when sort_diffs feature flag is disabled' do + before do + stub_feature_flags(sort_diffs: false) + end + + it 'does not sort diff files directory first' do + expect(diff_with_commits.diffs_in_batch(1, 10, diff_options: diff_options).diff_file_paths).to eq([ + '.DS_Store', + '.gitattributes', + '.gitignore', + '.gitmodules', + 'CHANGELOG', + 'README', + 'bar/branch-test.txt', + 'custom-highlighting/test.gitlab-custom', + 'encoding/iso8859.txt', + 'files/.DS_Store' + ]) + end + end + end + end + + describe '#diffs' do + let(:diff_options) { {} } + + shared_examples_for 'fetching full diffs' do + it 'returns diffs from repository comparison' do + expect_next_instance_of(Compare) do |comparison| + expect(comparison).to receive(:diffs) + .with(diff_options) + .and_call_original + end + + diff_with_commits.diffs(diff_options) + end + + it 'returns a Gitlab::Diff::FileCollection::Compare with full diffs' do + diffs = diff_with_commits.diffs(diff_options) + + expect(diffs).to be_a(Gitlab::Diff::FileCollection::Compare) + expect(diffs.diff_files.size).to eq(20) + end + end + + context 'when no persisted files available' do + before do + diff_with_commits.clean! + end + + it_behaves_like 'fetching full diffs' + end + + context 'when diff_options include ignore_whitespace_change' do + it_behaves_like 'fetching full diffs' do + let(:diff_options) do + { ignore_whitespace_change: true } + end + end + end + + context 'when persisted files available' do + it 'returns diffs' do + diffs = diff_with_commits.diffs(diff_options) + + expect(diffs).to be_a(Gitlab::Diff::FileCollection::MergeRequestDiff) + expect(diffs.diff_files.size).to eq(20) + end + + it 'sorts diff files directory first' do + diff_with_commits.update!(sorted: false) # Mark as unsorted so it'll re-order + + expect(diff_with_commits.diffs(diff_options).diff_file_paths).to eq([ + 'bar/branch-test.txt', + 'custom-highlighting/test.gitlab-custom', + 'encoding/iso8859.txt', + 'files/images/wm.svg', + 'files/js/commit.coffee', + 'files/lfs/lfs_object.iso', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/.DS_Store', + 'files/whitespace', + 'foo/bar/.gitkeep', + 'with space/README.md', + '.DS_Store', + '.gitattributes', + '.gitignore', + '.gitmodules', + 'CHANGELOG', + 'README', + 'gitlab-grack', + 'gitlab-shell' + ]) + end + + context 'when sort_diffs feature flag is disabled' do + before do + stub_feature_flags(sort_diffs: false) + end + + it 'does not sort diff files directory first' do + expect(diff_with_commits.diffs(diff_options).diff_file_paths).to eq([ + '.DS_Store', + '.gitattributes', + '.gitignore', + '.gitmodules', + 'CHANGELOG', + 'README', + 'bar/branch-test.txt', + 'custom-highlighting/test.gitlab-custom', + 'encoding/iso8859.txt', + 'files/.DS_Store', + 'files/images/wm.svg', + 'files/js/commit.coffee', + 'files/lfs/lfs_object.iso', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/whitespace', + 'foo/bar/.gitkeep', + 'gitlab-grack', + 'gitlab-shell', + 'with space/README.md' + ]) + end + end end end @@ -505,6 +649,68 @@ RSpec.describe MergeRequestDiff do expect(mr_diff.empty?).to be_truthy end + it 'persists diff files sorted directory first' do + mr_diff = create(:merge_request).merge_request_diff + diff_files_paths = mr_diff.merge_request_diff_files.map { |file| file.new_path.presence || file.old_path } + + expect(diff_files_paths).to eq([ + 'bar/branch-test.txt', + 'custom-highlighting/test.gitlab-custom', + 'encoding/iso8859.txt', + 'files/images/wm.svg', + 'files/js/commit.coffee', + 'files/lfs/lfs_object.iso', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/.DS_Store', + 'files/whitespace', + 'foo/bar/.gitkeep', + 'with space/README.md', + '.DS_Store', + '.gitattributes', + '.gitignore', + '.gitmodules', + 'CHANGELOG', + 'README', + 'gitlab-grack', + 'gitlab-shell' + ]) + end + + context 'when sort_diffs feature flag is disabled' do + before do + stub_feature_flags(sort_diffs: false) + end + + it 'persists diff files unsorted by directory first' do + mr_diff = create(:merge_request).merge_request_diff + diff_files_paths = mr_diff.merge_request_diff_files.map { |file| file.new_path.presence || file.old_path } + + expect(diff_files_paths).to eq([ + '.DS_Store', + '.gitattributes', + '.gitignore', + '.gitmodules', + 'CHANGELOG', + 'README', + 'bar/branch-test.txt', + 'custom-highlighting/test.gitlab-custom', + 'encoding/iso8859.txt', + 'files/.DS_Store', + 'files/images/wm.svg', + 'files/js/commit.coffee', + 'files/lfs/lfs_object.iso', + 'files/ruby/popen.rb', + 'files/ruby/regex.rb', + 'files/whitespace', + 'foo/bar/.gitkeep', + 'gitlab-grack', + 'gitlab-shell', + 'with space/README.md' + ]) + end + end + it 'expands collapsed diffs before saving' do mr_diff = create(:merge_request, source_branch: 'expand-collapse-lines', target_branch: 'master').merge_request_diff diff_file = mr_diff.merge_request_diff_files.find_by(new_path: 'expand-collapse/file-5.txt') diff --git a/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb new file mode 100644 index 00000000000..e086ce02942 --- /dev/null +++ b/spec/requests/api/graphql/ci/ci_cd_setting_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe 'Getting Ci Cd Setting' do + include GraphqlHelpers + + let_it_be_with_reload(:project) { create(:project, :repository) } + let_it_be(:current_user) { project.owner } + + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('ProjectCiCdSetting')} + QUERY + end + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('ciCdSettings', {}, fields) + ) + end + + let(:settings_data) { graphql_data['project']['ciCdSettings'] } + + context 'without permissions' do + let(:user) { create(:user) } + + before do + project.add_reporter(user) + post_graphql(query, current_user: user) + end + + it_behaves_like 'a working graphql query' + + specify { expect(settings_data).to be nil } + end + + context 'with project permissions' do + before do + post_graphql(query, current_user: current_user) + end + + it_behaves_like 'a working graphql query' + + specify { expect(settings_data['mergePipelinesEnabled']).to eql project.ci_cd_settings.merge_pipelines_enabled? } + specify { expect(settings_data['mergeTrainsEnabled']).to eql project.ci_cd_settings.merge_trains_enabled? } + specify { expect(settings_data['project']['id']).to eql "gid://gitlab/Project/#{project.id}" } + end +end |