summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-12-15 00:10:07 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-12-15 00:10:07 +0000
commitf68b31bd2c8812f4aa3654c6ab7271578c2ceea4 (patch)
tree7c62f11462cde7d6b0b14d8cbbfe93336f7a25c8
parent3a51d1d11d8282ec011f1a79fa10b1ce370e9933 (diff)
downloadgitlab-ce-f68b31bd2c8812f4aa3654c6ab7271578c2ceea4.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js10
-rw-r--r--app/assets/javascripts/editor/constants.js4
-rw-r--r--app/assets/javascripts/editor/editor_file_template_ext.js7
-rw-r--r--app/assets/javascripts/editor/editor_lite.js34
-rw-r--r--app/assets/javascripts/editor/editor_lite_extension_base.js11
-rw-r--r--app/assets/javascripts/editor/editor_markdown_ext.js14
-rw-r--r--app/finders/ci/daily_build_group_report_results_finder.rb13
-rw-r--r--app/graphql/types/ci/ci_cd_setting_type.rb20
-rw-r--r--app/graphql/types/project_type.rb5
-rw-r--r--app/models/merge_request_diff.rb58
-rw-r--r--app/policies/project_ci_cd_setting_policy.rb5
-rw-r--r--app/serializers/diff_file_base_entity.rb2
-rw-r--r--changelogs/unreleased/26552-sort-diff-files.yml5
-rw-r--r--changelogs/unreleased/282520-follow-up-enable-and-disable-merge-train-checkbox-based-on-pipelin.yml5
-rw-r--r--changelogs/unreleased/288312-editor-lite-extension-options.yml5
-rw-r--r--config/feature_flags/development/sort_diffs.yml8
-rw-r--r--db/migrate/20201202133606_add_sorted_to_merge_request_diffs.rb19
-rw-r--r--db/schema_migrations/202012021336061
-rw-r--r--db/structure.sql1
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql22
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json69
-rw-r--r--doc/api/graphql/reference/index.md9
-rw-r--r--doc/ci/pipelines/job_artifacts.md11
-rw-r--r--doc/development/documentation/styleguide/index.md5
-rw-r--r--doc/user/clusters/agent/repository.md9
-rw-r--r--doc/user/group/repositories_analytics/index.md7
-rw-r--r--doc/user/packages/dependency_proxy/index.md7
-rw-r--r--doc/user/project/settings/project_access_tokens.md2
-rw-r--r--lib/gitlab/auth/otp/strategies/forti_authenticator.rb5
-rw-r--r--lib/gitlab/git/diff_collection.rb6
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/features/merge_request/user_comments_on_diff_spec.rb24
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb10
-rw-r--r--spec/features/merge_request/user_views_diffs_file_by_file_spec.rb4
-rw-r--r--spec/features/merge_request/user_views_diffs_spec.rb4
-rw-r--r--spec/finders/ci/daily_build_group_report_results_finder_spec.rb87
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js17
-rw-r--r--spec/frontend/editor/editor_lite_extension_base_spec.js44
-rw-r--r--spec/frontend/editor/editor_lite_spec.js159
-rw-r--r--spec/frontend/editor/editor_markdown_ext_spec.js4
-rw-r--r--spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb21
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/models/merge_request_diff_spec.rb208
-rw-r--r--spec/requests/api/graphql/ci/ci_cd_setting_spec.rb50
47 files changed, 883 insertions, 139 deletions
diff --git a/Gemfile b/Gemfile
index b404810ff45..1ddf90a4cb4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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