summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-10 12:08:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-10 12:08:54 +0000
commit766b24b86ba1c5405d6a300f35062c33108941d4 (patch)
treeee9a661db63c6257ebce580882fe539bfce3c492
parent1385b54a3e44a90a463d4975bd639089be056778 (diff)
downloadgitlab-ce-766b24b86ba1c5405d6a300f35062c33108941d4.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/groups/components/invite_members_banner.vue10
-rw-r--r--app/assets/javascripts/groups/init_invite_members_banner.js3
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_area.vue2
-rw-r--r--app/assets/javascripts/static_site_editor/services/parse_source_file.js85
-rw-r--r--app/assets/javascripts/static_site_editor/services/parse_source_file_language_support.js17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js6
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb8
-rw-r--r--app/graphql/mutations/boards/issues/issue_move_list.rb4
-rw-r--r--app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb37
-rw-r--r--app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb21
-rw-r--r--app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb24
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/models/analytics/instance_statistics/measurement.rb12
-rw-r--r--app/models/merge_request.rb13
-rw-r--r--app/serializers/merge_request_poll_cached_widget_entity.rb10
-rw-r--r--app/views/admin/services/_form.html.haml2
-rw-r--r--app/views/groups/show.html.haml1
-rw-r--r--changelogs/unreleased/20748-fix-validation-on-external-wiki-service-template-form.yml5
-rw-r--r--changelogs/unreleased/241673-expose-instance-statistics-via-graphql.yml5
-rw-r--r--changelogs/unreleased/244848-skip-cleanup-for-forks.yml5
-rw-r--r--changelogs/unreleased/display-merged-commit-sha-in-fast-forward-mode.yml5
-rw-r--r--changelogs/unreleased/ensure-db-consistency-after-36321.yml5
-rw-r--r--config/feature_flags/development/usage_data_api.yml7
-rw-r--r--db/post_migrate/20200907124300_complete_namespace_settings_migration.rb35
-rw-r--r--db/schema_migrations/202009071243001
-rw-r--r--doc/.vale/gitlab/spelling-exceptions.txt1
-rw-r--r--doc/api/boards.md2
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql124
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json293
-rw-r--r--doc/api/graphql/reference/index.md10
-rw-r--r--doc/api/issues.md6
-rw-r--r--doc/development/feature_flags/development.md2
-rw-r--r--doc/development/telemetry/usage_ping.md22
-rw-r--r--doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md146
-rw-r--r--doc/raketasks/cleanup.md4
-rw-r--r--doc/user/analytics/img/delete_value_stream_v13.4.pngbin0 -> 29439 bytes
-rw-r--r--doc/user/analytics/value_stream_analytics.md13
-rw-r--r--doc/user/application_security/configuration/index.md12
-rw-r--r--doc/user/project/issue_board.md166
-rw-r--r--doc/user/project/issues/design_management.md40
-rw-r--r--doc/user/project/issues/img/design_todo_button_v13_4.pngbin0 -> 166635 bytes
-rw-r--r--doc/user/project/issues/sorting_issue_lists.md4
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/usage_data.rb29
-rw-r--r--lib/gitlab/cleanup/orphan_lfs_file_references.rb8
-rw-r--r--lib/gitlab/danger/helper.rb5
-rw-r--r--lib/gitlab/logger.rb3
-rw-r--r--lib/gitlab/quick_actions/issue_and_merge_request_actions.rb2
-rw-r--r--locale/gitlab.pot15
-rw-r--r--package.json1
-rw-r--r--spec/factories/instance_statistics/measurement.rb10
-rw-r--r--spec/features/merge_request/user_accepts_merge_request_spec.rb34
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_poll_cached_widget.json6
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js60
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js2
-rw-r--r--spec/frontend/static_site_editor/mock_data.js25
-rw-r--r--spec/frontend/static_site_editor/services/parse_source_file_language_support_spec.js20
-rw-r--r--spec/frontend/static_site_editor/services/parse_source_file_spec.js30
-rw-r--r--spec/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver_spec.rb47
-rw-r--r--spec/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum_spec.rb13
-rw-r--r--spec/graphql/types/admin/analytics/instance_statistics/measurement_type_spec.rb11
-rw-r--r--spec/graphql/types/query_type_spec.rb9
-rw-r--r--spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb40
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb24
-rw-r--r--spec/migrations/complete_namespace_settings_migration_spec.rb24
-rw-r--r--spec/models/analytics/instance_statistics/measurement_spec.rb31
-rw-r--r--spec/models/application_record_spec.rb68
-rw-r--r--spec/models/merge_request_spec.rb54
-rw-r--r--spec/requests/api/graphql/instance_statistics_measurements_spec.rb21
-rw-r--r--spec/requests/api/usage_data_spec.rb67
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb117
-rw-r--r--yarn.lock25
74 files changed, 1627 insertions, 352 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 02fe0a51d0b..a1f2210c5ce 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-47f676eea28871563414671e1016fb28b1b3e167
+d2e978f8e8f47a49c3bcfbd470b2f790e52c5ee2
diff --git a/app/assets/javascripts/groups/components/invite_members_banner.vue b/app/assets/javascripts/groups/components/invite_members_banner.vue
index 83a23134279..a2d18229c8b 100644
--- a/app/assets/javascripts/groups/components/invite_members_banner.vue
+++ b/app/assets/javascripts/groups/components/invite_members_banner.vue
@@ -1,20 +1,22 @@
<script>
import { GlBanner } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
export default {
components: {
GlBanner,
},
- inject: ['svgPath', 'inviteMembersPath'],
+ inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey'],
data() {
return {
- visible: true,
+ isDismissed: parseBoolean(getCookie(this.isDismissedKey)),
};
},
methods: {
handleClose() {
- this.visible = false;
+ setCookie(this.isDismissedKey, true);
+ this.isDismissed = true;
},
},
i18n: {
@@ -29,7 +31,7 @@ export default {
<template>
<gl-banner
- v-if="visible"
+ v-if="!isDismissed"
ref="banner"
:title="$options.i18n.title"
:button-text="$options.i18n.button_text"
diff --git a/app/assets/javascripts/groups/init_invite_members_banner.js b/app/assets/javascripts/groups/init_invite_members_banner.js
index dbc3ed65a4f..9117337895f 100644
--- a/app/assets/javascripts/groups/init_invite_members_banner.js
+++ b/app/assets/javascripts/groups/init_invite_members_banner.js
@@ -8,13 +8,14 @@ export default function initInviteMembersBanner() {
return false;
}
- const { svgPath, inviteMembersPath } = el.dataset;
+ const { svgPath, inviteMembersPath, isDismissedKey } = el.dataset;
return new Vue({
el,
provide: {
svgPath,
inviteMembersPath,
+ isDismissedKey,
},
render: createElement => createElement(InviteMembersBanner),
});
diff --git a/app/assets/javascripts/static_site_editor/components/edit_area.vue b/app/assets/javascripts/static_site_editor/components/edit_area.vue
index dc3d3fb6be3..7272e8eceb0 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_area.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_area.vue
@@ -68,7 +68,7 @@ export default {
return templatedContent;
},
onInputChange(newVal) {
- this.parsedSource.sync(newVal, this.isWysiwygMode);
+ this.parsedSource.syncContent(newVal, this.isWysiwygMode);
this.isModified = this.parsedSource.isModified();
},
onModeChange(mode) {
diff --git a/app/assets/javascripts/static_site_editor/services/parse_source_file.js b/app/assets/javascripts/static_site_editor/services/parse_source_file.js
index 57505d50d6e..5618e36e017 100644
--- a/app/assets/javascripts/static_site_editor/services/parse_source_file.js
+++ b/app/assets/javascripts/static_site_editor/services/parse_source_file.js
@@ -1,78 +1,49 @@
-import getFrontMatterLanguageDefinition from './parse_source_file_language_support';
+import grayMatter from 'gray-matter';
-const parseSourceFile = (raw, options = { frontMatterLanguage: 'yaml' }) => {
- const { open, close } = getFrontMatterLanguageDefinition(options.frontMatterLanguage);
- const anyChar = '[\\s\\S]';
- const frontMatterBlock = `^${open}$${anyChar}*?^${close}$`;
- const frontMatterRegex = new RegExp(`${frontMatterBlock}`, 'm');
- const preGroupedRegex = new RegExp(`(${anyChar}*?)(${frontMatterBlock})(\\s*)(${anyChar}*)`, 'm'); // preFrontMatter, frontMatter, spacing, and content
- let initial;
- let editable;
+const parseSourceFile = raw => {
+ const remake = source => grayMatter(source, {});
- const hasFrontMatter = source => frontMatterRegex.test(source);
+ let editable = remake(raw);
- const buildPayload = (source, header, spacing, body) => {
- return { raw: source, header, spacing, body };
- };
-
- const parse = source => {
- if (hasFrontMatter(source)) {
- const match = source.match(preGroupedRegex);
- const [, preFrontMatter, frontMatter, spacing, content] = match;
- const header = preFrontMatter + frontMatter;
-
- return buildPayload(source, header, spacing, content);
+ const syncContent = (newVal, isBody) => {
+ if (isBody) {
+ editable.content = newVal;
+ } else {
+ editable = remake(newVal);
}
-
- return buildPayload(source, '', '', source);
};
- const syncEditable = () => {
- /*
- We re-parse as markdown editing could have added non-body changes (preFrontMatter, frontMatter, or spacing).
- Re-parsing additionally gets us the desired body that was extracted from the potentially mutated editable.raw
- */
- editable = parse(editable.raw);
- };
+ const trimmedEditable = () => grayMatter.stringify(editable).trim();
- const refreshEditableRaw = () => {
- editable.raw = `${editable.header}${editable.spacing}${editable.body}`;
- };
+ const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96
- const sync = (newVal, isBodyToRaw) => {
- const editableKey = isBodyToRaw ? 'body' : 'raw';
- editable[editableKey] = newVal;
+ const matter = () => editable.matter;
- if (isBodyToRaw) {
- refreshEditableRaw();
- }
-
- syncEditable();
+ const syncMatter = newMatter => {
+ const targetMatter = newMatter.replace(/---/gm, ''); // TODO dynamic delimiter removal vs. hard code
+ const currentMatter = matter();
+ const currentContent = content();
+ const newSource = currentContent.replace(currentMatter, targetMatter);
+ syncContent(newSource);
+ editable.matter = newMatter;
};
- const frontMatter = () => editable.header;
+ const matterObject = () => editable.data;
- const setFrontMatter = val => {
- editable.header = val;
- refreshEditableRaw();
+ const syncMatterObject = obj => {
+ editable.data = obj;
};
- const content = (isBody = false) => {
- const editableKey = isBody ? 'body' : 'raw';
- return editable[editableKey];
- };
-
- const isModified = () => initial.raw !== editable.raw;
-
- initial = parse(raw);
- editable = parse(raw);
+ const isModified = () => trimmedEditable() !== raw;
return {
- frontMatter,
- setFrontMatter,
+ matter,
+ syncMatter,
+ matterObject,
+ syncMatterObject,
content,
+ syncContent,
isModified,
- sync,
};
};
diff --git a/app/assets/javascripts/static_site_editor/services/parse_source_file_language_support.js b/app/assets/javascripts/static_site_editor/services/parse_source_file_language_support.js
deleted file mode 100644
index ec0eaca81b8..00000000000
--- a/app/assets/javascripts/static_site_editor/services/parse_source_file_language_support.js
+++ /dev/null
@@ -1,17 +0,0 @@
-const frontMatterLanguageDefinitions = [
- { name: 'yaml', open: '---', close: '---' },
- { name: 'toml', open: '\\+\\+\\+', close: '\\+\\+\\+' },
- { name: 'json', open: '{', close: '}' },
-];
-
-const getFrontMatterLanguageDefinition = name => {
- const languageDefinition = frontMatterLanguageDefinitions.find(def => def.name === name);
-
- if (!languageDefinition) {
- throw new Error(`Unsupported front matter language: ${name}`);
- }
-
- return languageDefinition;
-};
-
-export default getFrontMatterLanguageDefinition;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index 5695e02bbab..78e40d16c22 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -45,8 +45,8 @@ export default class MergeRequestStore {
this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path;
this.mergeStatus = data.merge_status;
this.commitMessage = data.default_merge_commit_message;
- this.shortMergeCommitSha = data.short_merge_commit_sha;
- this.mergeCommitSha = data.merge_commit_sha;
+ this.shortMergeCommitSha = data.short_merged_commit_sha;
+ this.mergeCommitSha = data.merged_commit_sha;
this.commitMessageWithDescription = data.default_merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
@@ -135,7 +135,7 @@ export default class MergeRequestStore {
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergePath = data.merge_path;
this.canMerge = Boolean(data.merge_path);
- this.mergeCommitPath = data.merge_commit_path;
+ this.mergeCommitPath = data.merged_commit_path;
this.canPushToSourceBranch = data.can_push_to_source_branch;
if (data.work_in_progress !== undefined) {
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 1b18e5c80be..8aacfdce094 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -64,7 +64,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
render: ->(partial, locals) { view_to_html_string(partial, locals) }
}
- options = additional_attributes.merge(diff_view: diff_view)
+ options = additional_attributes.merge(diff_view: Feature.enabled?(:unified_diff_lines, @merge_request.project) ? "inline" : diff_view)
if @merge_request.project.context_commits_enabled?
options[:context_commits] = @merge_request.recent_context_commits
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 14a8a4c5961..547a3311873 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -428,7 +428,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42438')
end
- def reports_response(report_comparison)
+ def reports_response(report_comparison, pipeline = nil)
+ if pipeline&.active?
+ ::Gitlab::PollingInterval.set_header(response, interval: 3000)
+
+ render json: '', status: :no_content && return
+ end
+
case report_comparison[:status]
when :parsing
::Gitlab::PollingInterval.set_header(response, interval: 3000)
diff --git a/app/graphql/mutations/boards/issues/issue_move_list.rb b/app/graphql/mutations/boards/issues/issue_move_list.rb
index d4bf47af4cf..c953658bb35 100644
--- a/app/graphql/mutations/boards/issues/issue_move_list.rb
+++ b/app/graphql/mutations/boards/issues/issue_move_list.rb
@@ -29,11 +29,11 @@ module Mutations
argument :move_before_id, GraphQL::ID_TYPE,
required: false,
- description: 'ID of issue before which the current issue will be positioned at'
+ description: 'ID of issue that should be placed before the current issue'
argument :move_after_id, GraphQL::ID_TYPE,
required: false,
- description: 'ID of issue after which the current issue will be positioned at'
+ description: 'ID of issue that should be placed after the current issue'
def ready?(**args)
if move_arguments(args).blank?
diff --git a/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb b/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb
new file mode 100644
index 00000000000..aea3afa8ec5
--- /dev/null
+++ b/app/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Admin
+ module Analytics
+ module InstanceStatistics
+ class MeasurementsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::Admin::Analytics::InstanceStatistics::MeasurementType, null: true
+
+ argument :identifier, Types::Admin::Analytics::InstanceStatistics::MeasurementIdentifierEnum,
+ required: true,
+ description: 'The type of measurement/statistics to retrieve'
+
+ def resolve(identifier:)
+ authorize!
+
+ ::Analytics::InstanceStatistics::Measurement
+ .with_identifier(identifier)
+ .order_by_latest
+ end
+
+ private
+
+ def authorize!
+ admin? || raise_resource_not_available_error!
+ end
+
+ def admin?
+ context[:current_user].present? && context[:current_user].admin?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb b/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb
new file mode 100644
index 00000000000..13c67442c2e
--- /dev/null
+++ b/app/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+ module Admin
+ module Analytics
+ module InstanceStatistics
+ class MeasurementIdentifierEnum < BaseEnum
+ graphql_name 'MeasurementIdentifier'
+ description 'Possible identifier types for a measurement'
+
+ value 'PROJECTS', 'Project count', value: :projects
+ value 'USERS', 'User count', value: :users
+ value 'ISSUES', 'Issue count', value: :issues
+ value 'MERGE_REQUESTS', 'Merge request count', value: :merge_requests
+ value 'GROUPS', 'Group count', value: :groups
+ value 'PIPELINES', 'Pipeline count', value: :pipelines
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb b/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb
new file mode 100644
index 00000000000..d45341077a4
--- /dev/null
+++ b/app/graphql/types/admin/analytics/instance_statistics/measurement_type.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+# rubocop:disable Graphql/AuthorizeTypes
+
+module Types
+ module Admin
+ module Analytics
+ module InstanceStatistics
+ class MeasurementType < BaseObject
+ graphql_name 'InstanceStatisticsMeasurement'
+ description 'Represents a recorded measurement (object count) for the Admins'
+
+ field :recorded_at, Types::TimeType, null: true,
+ description: 'The time the measurement was recorded'
+
+ field :count, GraphQL::INT_TYPE, null: false,
+ description: 'Object count'
+
+ field :identifier, Types::Admin::Analytics::InstanceStatistics::MeasurementIdentifierEnum, null: false,
+ description: 'The type of objects being measured'
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 7709b18fb39..447ac63a294 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -76,6 +76,11 @@ module Types
argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue'
end
+ field :instance_statistics_measurements, Types::Admin::Analytics::InstanceStatistics::MeasurementType.connection_type,
+ null: true,
+ description: 'Get statistics on the instance',
+ resolver: Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsResolver
+
def design_management
DesignManagementObject.new(nil)
end
diff --git a/app/models/analytics/instance_statistics/measurement.rb b/app/models/analytics/instance_statistics/measurement.rb
index 162f2b461ed..fd917632e15 100644
--- a/app/models/analytics/instance_statistics/measurement.rb
+++ b/app/models/analytics/instance_statistics/measurement.rb
@@ -3,10 +3,20 @@
module Analytics
module InstanceStatistics
class Measurement < ApplicationRecord
- enum identifier: { projects: 1, users: 2 }
+ enum identifier: {
+ projects: 1,
+ users: 2,
+ issues: 3,
+ merge_requests: 4,
+ groups: 5,
+ pipelines: 6
+ }
validates :recorded_at, :identifier, :count, presence: true
validates :recorded_at, uniqueness: { scope: :identifier }
+
+ scope :order_by_latest, -> { order(recorded_at: :desc) }
+ scope :with_identifier, -> (identifier) { where(identifier: identifier) }
end
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index ccf9f501799..1f0d618d527 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1473,6 +1473,19 @@ class MergeRequest < ApplicationRecord
Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
end
+ def merged_commit_sha
+ return unless merged?
+
+ sha = merge_commit_sha || squash_commit_sha || diff_head_sha
+ sha.presence
+ end
+
+ def short_merged_commit_sha
+ if sha = merged_commit_sha
+ Commit.truncate_sha(sha)
+ end
+ end
+
def can_be_reverted?(current_user)
return false unless merge_commit
return false unless merged_at
diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb
index c51c08ab646..002be8be729 100644
--- a/app/serializers/merge_request_poll_cached_widget_entity.rb
+++ b/app/serializers/merge_request_poll_cached_widget_entity.rb
@@ -3,8 +3,8 @@
class MergeRequestPollCachedWidgetEntity < IssuableEntity
expose :auto_merge_enabled
expose :state
- expose :merge_commit_sha
- expose :short_merge_commit_sha
+ expose :merged_commit_sha
+ expose :short_merged_commit_sha
expose :merge_error
expose :public_merge_status, as: :merge_status
expose :merge_user_id
@@ -56,9 +56,9 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
presenter(merge_request).target_branch_tree_path
end
- expose :merge_commit_path do |merge_request|
- if merge_request.merge_commit_sha
- project_commit_path(merge_request.project, merge_request.merge_commit_sha)
+ expose :merged_commit_path do |merge_request|
+ if sha = merge_request.merged_commit_sha
+ project_commit_path(merge_request.project, sha)
end
end
diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index f2153e503af..7e6c708e75d 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -3,7 +3,7 @@
%p #{@service.description} template.
-= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form|
+= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form js-integration-settings-form' } do |form|
= render 'shared/service_settings', form: form, integration: @service
.footer-block.row-content-block
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 325504eb728..b0055ec81db 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -6,6 +6,7 @@
= content_for :group_invite_members_banner do
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
.js-group-invite-members-banner{ data: { svg_path: image_path('illustrations/merge_requests.svg'),
+ is_dismissed_key: "invite_#{@group.id}_#{current_user.id}",
invite_members_path: group_group_members_path(@group) } }
= content_for :meta_tags do
diff --git a/changelogs/unreleased/20748-fix-validation-on-external-wiki-service-template-form.yml b/changelogs/unreleased/20748-fix-validation-on-external-wiki-service-template-form.yml
new file mode 100644
index 00000000000..eda8ff48d87
--- /dev/null
+++ b/changelogs/unreleased/20748-fix-validation-on-external-wiki-service-template-form.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Fix validation on External Wiki service template form
+merge_request: 41964
+author:
+type: fixed
diff --git a/changelogs/unreleased/241673-expose-instance-statistics-via-graphql.yml b/changelogs/unreleased/241673-expose-instance-statistics-via-graphql.yml
new file mode 100644
index 00000000000..b5fbb43ebaa
--- /dev/null
+++ b/changelogs/unreleased/241673-expose-instance-statistics-via-graphql.yml
@@ -0,0 +1,5 @@
+---
+title: Expose Instance Statistics measurements (object counts) via GraphQL
+merge_request: 40871
+author:
+type: added
diff --git a/changelogs/unreleased/244848-skip-cleanup-for-forks.yml b/changelogs/unreleased/244848-skip-cleanup-for-forks.yml
new file mode 100644
index 00000000000..eac3da71aae
--- /dev/null
+++ b/changelogs/unreleased/244848-skip-cleanup-for-forks.yml
@@ -0,0 +1,5 @@
+---
+title: Refuse to perform an LFS clean on projects that are fork roots
+merge_request: 41703
+author:
+type: fixed
diff --git a/changelogs/unreleased/display-merged-commit-sha-in-fast-forward-mode.yml b/changelogs/unreleased/display-merged-commit-sha-in-fast-forward-mode.yml
new file mode 100644
index 00000000000..7e596847beb
--- /dev/null
+++ b/changelogs/unreleased/display-merged-commit-sha-in-fast-forward-mode.yml
@@ -0,0 +1,5 @@
+---
+title: Display merged commit sha in fast-forward merge mode
+merge_request: 41369
+author: Mycroft Kang @TaehyeokKang
+type: added
diff --git a/changelogs/unreleased/ensure-db-consistency-after-36321.yml b/changelogs/unreleased/ensure-db-consistency-after-36321.yml
new file mode 100644
index 00000000000..4e388c1c726
--- /dev/null
+++ b/changelogs/unreleased/ensure-db-consistency-after-36321.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure namespace settings are backfilled via migration
+merge_request: 41679
+author:
+type: other
diff --git a/config/feature_flags/development/usage_data_api.yml b/config/feature_flags/development/usage_data_api.yml
new file mode 100644
index 00000000000..0976b27d417
--- /dev/null
+++ b/config/feature_flags/development/usage_data_api.yml
@@ -0,0 +1,7 @@
+---
+name: usage_data_api
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41301
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235459
+group: group::telemetry
+type: development
+default_enabled: false
diff --git a/db/post_migrate/20200907124300_complete_namespace_settings_migration.rb b/db/post_migrate/20200907124300_complete_namespace_settings_migration.rb
new file mode 100644
index 00000000000..5881869ee3c
--- /dev/null
+++ b/db/post_migrate/20200907124300_complete_namespace_settings_migration.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+class CompleteNamespaceSettingsMigration < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+ BATCH_SIZE = 10000
+
+ class Namespace < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'namespaces'
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('BackfillNamespaceSettings')
+
+ ensure_data_migration
+ end
+
+ def down
+ # no-op
+ end
+
+ private
+
+ def ensure_data_migration
+ Namespace.each_batch(of: BATCH_SIZE) do |query|
+ missing_count = query.where("NOT EXISTS (SELECT 1 FROM namespace_settings WHERE namespace_settings.namespace_id=namespaces.id)").limit(1).size
+ if missing_count > 0
+ min, max = query.pluck("MIN(id), MAX(id)").flatten
+ # we expect low record count so inline execution is fine.
+ Gitlab::BackgroundMigration::BackfillNamespaceSettings.new.perform(min, max)
+ end
+ end
+ end
+end
diff --git a/db/schema_migrations/20200907124300 b/db/schema_migrations/20200907124300
new file mode 100644
index 00000000000..a156f95f428
--- /dev/null
+++ b/db/schema_migrations/20200907124300
@@ -0,0 +1 @@
+2311967a9f68e1a428662e0231752ad0d844063d66cca895211d38f9ae928d94 \ No newline at end of file
diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt
index 2b855054246..f269d02c407 100644
--- a/doc/.vale/gitlab/spelling-exceptions.txt
+++ b/doc/.vale/gitlab/spelling-exceptions.txt
@@ -76,6 +76,7 @@ Citus
clonable
Cloudwatch
Cobertura
+Codepen
Cognito
colocated
colocating
diff --git a/doc/api/boards.md b/doc/api/boards.md
index a370205aa01..12ebbcf916a 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -455,7 +455,7 @@ POST /projects/:id/boards/:board_id/lists
NOTE: **Note:**
Label, assignee and milestone arguments are mutually exclusive,
that is, only one of them are accepted in a request.
-Check the [Issue Board docs](../user/project/issue_board.md#summary-of-features-per-tier)
+Check the [Issue Board docs](../user/project/issue_board.md)
for more information regarding the required license for each list type.
```shell
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 18d35c3e114..210c10e0949 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -7411,6 +7411,61 @@ type InstanceSecurityDashboard {
}
"""
+Represents a recorded measurement (object count) for the Admins
+"""
+type InstanceStatisticsMeasurement {
+ """
+ Object count
+ """
+ count: Int!
+
+ """
+ The type of objects being measured
+ """
+ identifier: MeasurementIdentifier!
+
+ """
+ The time the measurement was recorded
+ """
+ recordedAt: Time
+}
+
+"""
+The connection type for InstanceStatisticsMeasurement.
+"""
+type InstanceStatisticsMeasurementConnection {
+ """
+ A list of edges.
+ """
+ edges: [InstanceStatisticsMeasurementEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [InstanceStatisticsMeasurement]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type InstanceStatisticsMeasurementEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: InstanceStatisticsMeasurement
+}
+
+"""
Incident severity
"""
enum IssuableSeverity {
@@ -7872,12 +7927,12 @@ input IssueMoveListInput {
iid: String!
"""
- ID of issue after which the current issue will be positioned at
+ ID of issue that should be placed after the current issue
"""
moveAfterId: ID
"""
- ID of issue before which the current issue will be positioned at
+ ID of issue that should be placed before the current issue
"""
moveBeforeId: ID
@@ -9044,6 +9099,41 @@ type MarkAsSpamSnippetPayload {
snippet: Snippet
}
+"""
+Possible identifier types for a measurement
+"""
+enum MeasurementIdentifier {
+ """
+ Group count
+ """
+ GROUPS
+
+ """
+ Issue count
+ """
+ ISSUES
+
+ """
+ Merge request count
+ """
+ MERGE_REQUESTS
+
+ """
+ Pipeline count
+ """
+ PIPELINES
+
+ """
+ Project count
+ """
+ PROJECTS
+
+ """
+ User count
+ """
+ USERS
+}
+
interface MemberInterface {
"""
GitLab::Access level
@@ -13511,6 +13601,36 @@ type Query {
instanceSecurityDashboard: InstanceSecurityDashboard
"""
+ Get statistics on the instance
+ """
+ instanceStatisticsMeasurements(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ The type of measurement/statistics to retrieve
+ """
+ identifier: MeasurementIdentifier!
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+ ): InstanceStatisticsMeasurementConnection
+
+ """
Find an issue
"""
issue(
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index e28c0201e7e..9729d8e799a 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -20448,6 +20448,181 @@
"possibleTypes": null
},
{
+ "kind": "OBJECT",
+ "name": "InstanceStatisticsMeasurement",
+ "description": "Represents a recorded measurement (object count) for the Admins",
+ "fields": [
+ {
+ "name": "count",
+ "description": "Object count",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "identifier",
+ "description": "The type of objects being measured",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "MeasurementIdentifier",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "recordedAt",
+ "description": "The time the measurement was recorded",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "InstanceStatisticsMeasurementConnection",
+ "description": "The connection type for InstanceStatisticsMeasurement.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "InstanceStatisticsMeasurementEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "InstanceStatisticsMeasurement",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "InstanceStatisticsMeasurementEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "InstanceStatisticsMeasurement",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "SCALAR",
"name": "Int",
"description": "Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.",
@@ -21730,7 +21905,7 @@
},
{
"name": "moveBeforeId",
- "description": "ID of issue before which the current issue will be positioned at",
+ "description": "ID of issue that should be placed before the current issue",
"type": {
"kind": "SCALAR",
"name": "ID",
@@ -21740,7 +21915,7 @@
},
{
"name": "moveAfterId",
- "description": "ID of issue after which the current issue will be positioned at",
+ "description": "ID of issue that should be placed after the current issue",
"type": {
"kind": "SCALAR",
"name": "ID",
@@ -25094,6 +25269,53 @@
"possibleTypes": null
},
{
+ "kind": "ENUM",
+ "name": "MeasurementIdentifier",
+ "description": "Possible identifier types for a measurement",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": [
+ {
+ "name": "PROJECTS",
+ "description": "Project count",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "USERS",
+ "description": "User count",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ISSUES",
+ "description": "Issue count",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "MERGE_REQUESTS",
+ "description": "Merge request count",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "GROUPS",
+ "description": "Group count",
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "PIPELINES",
+ "description": "Pipeline count",
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "possibleTypes": null
+ },
+ {
"kind": "INTERFACE",
"name": "MemberInterface",
"description": null,
@@ -39661,6 +39883,73 @@
"deprecationReason": null
},
{
+ "name": "instanceStatisticsMeasurements",
+ "description": "Get statistics on the instance",
+ "args": [
+ {
+ "name": "identifier",
+ "description": "The type of measurement/statistics to retrieve",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "ENUM",
+ "name": "MeasurementIdentifier",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "InstanceStatisticsMeasurementConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "issue",
"description": "Find an issue",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 086a2dac6d0..73fe608153b 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1100,6 +1100,16 @@ Represents a Group Membership
| `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade |
| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity from projects selected in Instance Security Dashboard |
+## InstanceStatisticsMeasurement
+
+Represents a recorded measurement (object count) for the Admins
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `count` | Int! | Object count |
+| `identifier` | MeasurementIdentifier! | The type of objects being measured |
+| `recordedAt` | Time | The time the measurement was recorded |
+
## Issue
| Name | Type | Description |
diff --git a/doc/api/issues.md b/doc/api/issues.md
index b1ab5c96fcb..df9eaab7962 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -1181,9 +1181,9 @@ PUT /projects/:id/issues/:issue_iid/reorder
| Attribute | Type | Required | Description |
|-------------|---------|----------|--------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `issue_iid` | integer | yes | The internal ID of a project's issue |
-| `move_after_id` | integer | no | The ID of a project's issue to move this issue after |
-| `move_before_id` | integer | no | The ID of a project's issue to move this issue before |
+| `issue_iid` | integer | yes | The internal ID of the project's issue |
+| `move_after_id` | integer | no | The ID of a project's issue that should be placed after this issue |
+| `move_before_id` | integer | no | The ID of a project's issue that should be placed before this issue |
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/85/reorder?move_after_id=51&move_before_id=92"
diff --git a/doc/development/feature_flags/development.md b/doc/development/feature_flags/development.md
index b7923cfc268..b5a2da1b3c5 100644
--- a/doc/development/feature_flags/development.md
+++ b/doc/development/feature_flags/development.md
@@ -68,7 +68,7 @@ should be a one-to-one mapping of `licensed` feature flags to licensed features.
`licensed` feature flags must be `default_enabled: true`, because that's the only
supported option in the current implementation. This is under development as per
-the [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/218667.
+the [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/218667).
The `licensed` type has a dedicated set of functions to check if a licensed
feature is available for a project or namespace. This check validates
diff --git a/doc/development/telemetry/usage_ping.md b/doc/development/telemetry/usage_ping.md
index 6297518c976..aab5d93816e 100644
--- a/doc/development/telemetry/usage_ping.md
+++ b/doc/development/telemetry/usage_ping.md
@@ -312,6 +312,28 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
end
```
+1. Track event using `UsageData` API
+
+ Increment unique users count using Redis HLL, for given event name.
+
+ In order to be able to increment the values the related feature `usage_data<event_name>` should be enabled.
+
+ ```plaintext
+ POST /usage_data/increment_unique_users
+ ```
+
+ | Attribute | Type | Required | Description |
+ | :-------- | :--- | :------- | :---------- |
+ | `event` | string | yes | The event name it should be tracked |
+
+ Response
+
+ Return 200 if tracking failed for any reason.
+
+ - `401 Unauthorized` if user is not authenticated
+ - `400 Bad request` if event parameter is missing
+ - `200` if event was tracked or any errors
+
1. Track event using base module `Gitlab::UsageDataCounters::HLLRedisCounter.track_event(entity_id, event_name)`.
Arguments:
diff --git a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md
index 4f89aa4e4d8..7ac0a00fcff 100644
--- a/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md
+++ b/doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md
@@ -135,9 +135,9 @@ docker stop gitlab-gitaly-ha praefect postgres gitaly3 gitaly2 gitaly1
docker rm gitlab-gitaly-ha praefect postgres gitaly3 gitaly2 gitaly1
```
-## Guide to run and debug monitor tests
+## Guide to run and debug Monitor tests
-## How to set up
+### How to set up
To run the Monitor tests locally, against the GDK, please follow the preparation steps below:
@@ -149,7 +149,7 @@ To enable Auto DevOps in GDK, follow the [associated setup](https://gitlab.com/g
You might see NGINX issues when you run `gdk start` or `gdk restart`. In that case, run `sft login` to revalidate your credentials and regain access the QA Tunnel.
-## How to run
+### How to run
Navigate to the folder in `/your-gdk/gitlab/qa` and issue the command:
@@ -174,7 +174,7 @@ At the moment of this writing, there are two specs which run monitor tests:
-`qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb` - has the specs of features in GitLab Core
-`qa/specs/features/ee/browser_ui/8_monitor/all_monitor_features_spec.rb` - has the specs of features for paid GitLab (Enterprise Edition)
-## How to debug
+### How to debug
The monitor tests follow this setup flow:
@@ -187,7 +187,7 @@ The monitor tests follow this setup flow:
The test requires a number of components. The setup requires time to collect the metrics of a real deployment.
The complexity of the setup may lead to problems unrelated to the app. The following sections include common strategies to debug possible issues.
-### Deployment with Auto DevOps
+#### Deployment with Auto DevOps
When debugging issues in the CI or locally in the CLI, open the Kubernetes job in the pipeline.
In the job log window, click on the top right icon labeled as *"Show complete raw"* to reveal raw job logs.
@@ -205,7 +205,7 @@ The long test setup does not take screenshots of failures, which is a known [iss
However, if the spec fails (after a successful deployment) then you should be able to find screenshots which display the feature failure.
To access them in CI, go to the main job log window, look on the left side panel's Job artifacts section, and click Browse.
-### Common issues
+#### Common issues
**Container Registry**
@@ -259,3 +259,137 @@ gitlab-managed-apps install-runner 0/1 Evicted
```
You can free some memory with either of the following commands: `docker prune system` or `docker prune volume`.
+
+## Geo tests
+
+Geo end-to-end tests can run locally against a [Geo GDK setup](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/master/doc/howto/geo.md) or on Geo spun up in Docker containers.
+
+### Using Geo GDK
+
+Run from the [`qa/` directory](https://gitlab.com/gitlab-org/gitlab/-/blob/f7272b77e80215c39d1ffeaed27794c220dbe03f/qa) with both GDK Geo primary and Geo secondary instances running:
+
+```shell
+CHROME_HEADLESS=false bundle exec bin/qa QA::EE::Scenario::Test::Geo --primary-address http://localhost:3001 --secondary-address http://localhost:3002 --without-setup
+```
+
+### Using Geo in Docker
+
+You can use [GitLab-QA Orchestrator](https://gitlab.com/gitlab-org/gitlab-qa) to orchestrate two GitLab containers and configure them as a Geo setup.
+
+Geo requires an EE license. To visit the Geo sites in your browser, you will need a reverse proxy server (for example, [NGINX](https://www.nginx.com/)).
+
+1. Export your EE license
+
+ ```shell
+ export EE_LICENSE=$(cat <path/to/your/gitlab_license>)
+ ```
+
+1. (Optional) Pull the GitLab image
+
+ This step is optional because pulling the Docker image is part of the [`Test::Integration::Geo` orchestrated scenario](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/d8c5c40607c2be0eda58bbca1b9f534b00889a0b/lib/gitlab/qa/scenario/test/integration/geo.rb). However, it's easier to monitor the download progress if you pull the image first, and the scenario will skip this step after checking that the image is up to date.
+
+ ```shell
+ # For the most recent nightly image
+ docker pull gitlab/gitlab-ee:nightly
+
+ # For a specific release
+ docker pull gitlab/gitlab-ee:13.0.10-ee.0
+
+ # For a specific image
+ docker pull registry.gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:examplesha123456789
+ ```
+
+1. Run the [`Test::Integration::Geo` orchestrated scenario](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/d8c5c40607c2be0eda58bbca1b9f534b00889a0b/lib/gitlab/qa/scenario/test/integration/geo.rb) with the `--no-teardown` option to build the GitLab containers, configure the Geo setup, and run Geo end-to-end tests. Running the tests after the Geo setup is complete is optional; the containers will keep running after you stop the tests.
+
+ ```shell
+ # Using the most recent nightly image
+ gitlab-qa Test::Integration::Geo EE --no-teardown
+
+ # Using a specific GitLab release
+ gitlab-qa Test::Integration::Geo EE:13.0.10-ee.0 --no-teardown
+
+ # Using a full image address
+ GITLAB_QA_ACCESS_TOKEN=your-token-here gitlab-qa Test::Integration::Geo registry.gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:examplesha123456789 --no-teardown
+ ```
+
+ You can use the `--no-tests` option to build the containers only, and then run the [`EE::Scenario::Test::Geo` scenario](https://gitlab.com/gitlab-org/gitlab/-/blob/f7272b77e80215c39d1ffeaed27794c220dbe03f/qa/qa/ee/scenario/test/geo.rb) from your GDK to complete setup and run tests. However, there might be configuration issues if your GDK and the containers are based on different GitLab versions. With the `--no-teardown` option, GitLab-QA uses the same GitLab version for the GitLab containers and the GitLab QA container used to configure the Geo setup.
+
+1. To visit the Geo sites in your browser, proxy requests to the hostnames used inside the containers. NGINX is used as the reverse proxy server for this example.
+
+ _Map the hostnames to the local IP in `/etc/hosts` file on your machine:_
+
+ ```plaintext
+ 127.0.0.1 gitlab-primary.geo gitlab-secondary.geo
+ ```
+
+ _Note the assigned ports:_
+
+ ```shell
+ $ docker port gitlab-primary
+
+ 80/tcp -> 0.0.0.0:32768
+
+ $ docker port gitlab-secondary
+
+ 80/tcp -> 0.0.0.0:32769
+ ```
+
+ _Configure the reverse proxy server with the assigned ports in `nginx.conf` file (usually found in `/usr/local/etc/nginx` on a Mac):_
+
+ ```plaintext
+ server {
+ server_name gitlab-primary.geo;
+ location / {
+ proxy_pass http://localhost:32768; # Change port to your assigned port
+ proxy_set_header Host gitlab-primary.geo;
+ }
+ }
+
+ server {
+ server_name gitlab-secondary.geo;
+ location / {
+ proxy_pass http://localhost:32769; # Change port to your assigned port
+ proxy_set_header Host gitlab-secondary.geo;
+ }
+ }
+ ```
+
+ _Start or reload the reverse proxy server:_
+
+ ```shell
+ sudo nginx
+ # or
+ sudo nginx -s reload
+ ```
+
+1. To run end-to-end tests from your local GDK, run the [`EE::Scenario::Test::Geo` scenario](https://gitlab.com/gitlab-org/gitlab/-/blob/f7272b77e80215c39d1ffeaed27794c220dbe03f/qa/qa/ee/scenario/test/geo.rb) from the [`gitlab/qa/` directory](https://gitlab.com/gitlab-org/gitlab/-/blob/f7272b77e80215c39d1ffeaed27794c220dbe03f/qa). Include `--without-setup` to skip the Geo configuration steps.
+
+ ```shell
+ QA_DEBUG=true GITLAB_QA_ACCESS_TOKEN=[add token here] GITLAB_QA_ADMIN_ACCESS_TOKEN=[add token here] bundle exec bin/qa QA::EE::Scenario::Test::Geo \
+ --primary-address http://gitlab-primary.geo \
+ --secondary-address http://gitlab-secondary.geo \
+ --without-setup
+ ```
+
+ If the containers need to be configured first (for example, if you used the `--no-tests` option in the previous step), run the `QA::EE::Scenario::Test::Geo scenario` as shown below to first do the Geo configuration steps, and then run Geo end-to-end tests. Make sure that `EE_LICENSE` is (still) defined in your shell session.
+
+ ```shell
+ QA_DEBUG=true bundle exec bin/qa QA::EE::Scenario::Test::Geo \
+ --primary-address http://gitlab-primary.geo \
+ --primary-name gitlab-primary \
+ --secondary-address http://gitlab-secondary.geo \
+ --secondary-name gitlab-secondary
+ ```
+
+1. Stop and remove containers
+
+ ```shell
+ docker stop gitlab-primary gitlab-secondary
+ docker rm gitlab-primary gitlab-secondary
+ ```
+
+#### Notes
+
+- You can find the full image address from a pipeline by [following these instructions](https://about.gitlab.com/handbook/engineering/quality/guidelines/tips-and-tricks/#running-gitlab-qa-pipeline-against-a-specific-gitlab-release). You might be prompted to set the `GITLAB_QA_ACCESS_TOKEN` variable if you specify the full image address.
+- You can increase the wait time for replication by setting `GEO_MAX_FILE_REPLICATION_TIME` and `GEO_MAX_DB_REPLICATION_TIME`. The default is 120 seconds.
+- To save time during tests, create a Personal Access Token with API access on the Geo primary node, and pass that value in as `GITLAB_QA_ACCESS_TOKEN` and `GITLAB_QA_ADMIN_ACCESS_TOKEN`.
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index c4046b36c55..340f58057a6 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -10,6 +10,10 @@ DANGER: **Danger:**
Do not run this within 12 hours of a GitLab upgrade. This is to ensure that all background migrations
have finished, which otherwise may lead to data loss.
+CAUTION: **WARNING:**
+Removing LFS files from a project with forks is currently unsafe. The rake task
+will refuse to run on projects with forks.
+
When you remove LFS files from a repository's history, they become orphaned and continue to consume
disk space. With this Rake task, you can remove invalid references from the database, which
will allow garbage collection of LFS files.
diff --git a/doc/user/analytics/img/delete_value_stream_v13.4.png b/doc/user/analytics/img/delete_value_stream_v13.4.png
new file mode 100644
index 00000000000..c97fcb76343
--- /dev/null
+++ b/doc/user/analytics/img/delete_value_stream_v13.4.png
Binary files differ
diff --git a/doc/user/analytics/value_stream_analytics.md b/doc/user/analytics/value_stream_analytics.md
index 7c83f5a0e0c..479acdb149a 100644
--- a/doc/user/analytics/value_stream_analytics.md
+++ b/doc/user/analytics/value_stream_analytics.md
@@ -313,6 +313,19 @@ To create a value stream:
![New value stream](img/new_value_stream_v13_3.png "Creating a new value stream")
+### Deleting a value stream
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/221205) in GitLab 13.4.
+
+To delete a custom value stream:
+
+1. Navigate to your group's **Analytics > Value Stream**.
+1. Click the Value stream dropdown and select the value stream you would like to delete.
+1. Click the **Delete (name of value stream)**.
+1. Click the **Delete** button to confirm.
+
+![Delete value stream](img/delete_value_stream_v13.4.png "Deleting a custom value stream")
+
### Disabling custom value streams
Custom value streams are enabled by default. If you have a self-managed instance, an
diff --git a/doc/user/application_security/configuration/index.md b/doc/user/application_security/configuration/index.md
index 2900bb24a90..a6ad701360e 100644
--- a/doc/user/application_security/configuration/index.md
+++ b/doc/user/application_security/configuration/index.md
@@ -15,18 +15,22 @@ The Security Configuration page displays the configuration state of each securit
current project.
To view a project's security configuration, go to the project's home page,
-then in the left sidebar, go to **Security & Compliance** > **Configuration**.
+then in the left sidebar go to **Security & Compliance > Configuration**.
-## Status
+For each security control the page displays:
+
+- **Status** - Status of the security control: enabled, not enabled, or available.
+- **Manage** - A management option or a link to the documentation.
-For each security control, the page displays the status and either a management option or a
-documentation link.
+## Status
The status of each security control is determined by the project's latest default branch
[CI pipeline](../../../ci/pipelines/index.md).
If a job with the expected security report artifact exists in the pipeline, the feature's status is
_enabled_.
+For SAST, click **View history** to see the `.gitlab-ci.yml` file’s history.
+
NOTE: **Note:**
If the latest pipeline used [Auto DevOps](../../../topics/autodevops/index.md),
all security features are configured by default.
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 963c3bfee25..f8172a0f988 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -15,59 +15,45 @@ organize, and visualize a workflow for a feature or product release.
It can be used as a [Kanban](https://en.wikipedia.org/wiki/Kanban_(development)) or a
[Scrum](https://en.wikipedia.org/wiki/Scrum_(software_development)) board.
-It pairs issue tracking and project management,
-keeping everything in the same place, so that you don't need to jump
-between different platforms to organize your workflow.
+It pairs issue tracking and project management, keeping everything in the same place,
+so that you don't need to jump between different platforms to organize your workflow.
-With issue boards, you organize your issues in lists that correspond to
-their assigned labels, visualizing issues designed as cards throughout those lists.
+Issue boards build on the existing [issue tracking functionality](issues/index.md#issues-list) and
+[labels](labels.md). Your issues appear as cards in vertical lists, organized by their assigned
+labels, [milestones](#milestone-lists), or [assignees](#assignee-lists).
-You define your process, and GitLab organizes it for you. You add your labels
-then create the corresponding list to pull in your existing issues. When
-you're ready, you can drag and drop your issue cards from one step to the next.
+Issue boards help you to visualize and manage your entire process in GitLab.
+You add your labels, and then create the corresponding list for your existing issues.
+When you're ready, you can drag your issue cards from one step to another one.
-![GitLab issue board - Core](img/issue_boards_core.png)
-
-<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
-Watch a [video presentation](https://youtu.be/UWsJ8tkHAa8) of
-the Issue Board feature (introduced in GitLab 8.11 - August 2016).
-
-### Advanced features of issue boards
-
-- Create multiple issue boards per project.
-- Create multiple issue boards per group. **(PREMIUM)**
-- Add lists for [assignees](#assignee-lists) and [milestones](#milestone-lists). **(PREMIUM)**
-
-Check all the [GitLab Enterprise features for issue boards](#gitlab-enterprise-features-for-issue-boards).
+An issue board can show you what issues your team is working on, who is assigned to each,
+and where in the workflow those issues are.
-![GitLab issue boards - Premium](img/issue_boards_premium.png)
+To let your team members organize their own workflows, use
+[multiple issue boards](#use-cases-for-multiple-issue-boards). This allows creating multiple issue
+boards in the same project.
-## How it works
+![GitLab issue board - Core](img/issue_boards_core.png)
-The Issue Board feature builds on GitLab's existing
-[issue tracking functionality](issues/index.md#issues-list) and
-[labels](labels.md) by using them as lists of the Scrum board.
+Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
+as shown in the following table:
-With issue boards you can have a different view of your issues while
-maintaining the same filtering and sorting abilities you see across the
-issue tracker. An issue board is based on its project's label structure, so it
-applies the same descriptive labels to indicate placement on the board, keeping
-consistency throughout the entire development lifecycle.
+| Tier | Number of project issue boards | Number of [group issue boards](#group-issue-boards) | [Configurable issue boards](#configurable-issue-boards) | [Assignee lists](#assignee-lists) |
+|------------------|--------------------------------|------------------------------|---------------------------|----------------|
+| Core / Free | Multiple | 1 | No | No |
+| Starter / Bronze | Multiple | 1 | Yes | No |
+| Premium / Silver | Multiple | Multiple | Yes | Yes |
+| Ultimate / Gold | Multiple | Multiple | Yes | Yes |
-An issue board shows you what issues your team is working on, who is assigned to each,
-and where in the workflow those issues are.
+To learn more, visit [GitLab Enterprise features for issue boards](#gitlab-enterprise-features-for-issue-boards) below.
-You create issues, host code, perform reviews, build, test,
-and deploy from one single platform. Issue boards help you to visualize
-and manage the entire process in GitLab.
+![GitLab issue board - Premium](img/issue_boards_premium.png)
-With [multiple issue boards](#use-cases-for-multiple-issue-boards),
-you go even further, as you can not only keep yourself and your project
-organized from a broader perspective with one issue board per project,
-but also allow your team members to organize their own workflow by creating
-multiple issue boards within the same project.
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+Watch a [video presentation](https://youtu.be/vjccjHI7aGI) of
+the Issue Board feature.
-## Use cases
+## Issue boards use cases
You can tailor GitLab issue boards to your own preferred workflow.
Here are some common use cases for issue boards.
@@ -138,8 +124,7 @@ to improve their workflow with multiple boards.
#### Quick assignments
-Create lists for each of your team members and quickly drag and drop issues onto each team member's
-list.
+Create lists for each of your team members and quickly drag issues onto each team member's list.
## Issue board terminology
@@ -172,22 +157,36 @@ card includes:
Users with the [Reporter and higher roles](../permissions.md) can use all the functionality of the
Issue Board feature to create or delete lists and drag issues from one list to another.
-## GitLab Enterprise features for issue boards
+## How GitLab orders issues in a list
-GitLab issue boards are available on GitLab Core and GitLab.com Free tiers, but some
-advanced functionality is present in [higher tiers only](https://about.gitlab.com/pricing/).
+When visiting a board, issues appear ordered in any list. You're able to change
+that order by dragging the issues. The changed order is saved, so that anybody who visits the same
+board later sees the reordering, with some exceptions.
-### Summary of features per tier
+The first time a given issue appears in any board (that is, the first time a user
+loads a board containing that issue), it is ordered in relation to other issues in that list
+according to [label priority](labels.md#label-priority).
-Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
-as shown in the following table:
+At this point, that issue is assigned a relative order value by the system,
+representing its relative order with respect to the other issues in the list. Any time
+you reorder that issue by dragging, its relative order value changes accordingly.
-| Tier | Number of Project issue boards | Number of Group issue boards | Configurable issue boards | Assignee lists |
-|------------------|--------------------------------|------------------------------|---------------------------|----------------|
-| Core / Free | Multiple | 1 | No | No |
-| Starter / Bronze | Multiple | 1 | Yes | No |
-| Premium / Silver | Multiple | Multiple | Yes | Yes |
-| Ultimate / Gold | Multiple | Multiple | Yes | Yes |
+Also, any time that issue appears in any board when it's loaded by a user,
+the updated relative order value is used for the ordering. It's only the first
+time an issue appears that it takes from the priority order mentioned above. This means that
+if issue `A` is reordered by dragging to be above issue `B` by any user in
+a given board inside your GitLab instance, any time those two issues are subsequently
+loaded in any board in the same instance (could be a different project board or a different group
+board, for example), that ordering is maintained.
+
+This ordering also affects [issue lists](issues/sorting_issue_lists.md).
+Changing the order in an issue board changes the ordering in an issue list,
+and vice versa.
+
+## GitLab Enterprise features for issue boards
+
+GitLab issue boards are available on GitLab Core and GitLab.com Free tiers, but some
+advanced functionality is present in [higher tiers only](https://about.gitlab.com/pricing/).
### Multiple issue boards
@@ -248,6 +247,10 @@ clicking **View scope**.
![Viewing board configuration](img/issue_board_view_scope.png)
+<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
+Watch a [video presentation](https://youtu.be/m5UTNCSqaDk) of
+the Configurable Issue Board feature.
+
### Focus mode
> - [Introduced]((https://about.gitlab.com/releases/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep)) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.1.
@@ -362,7 +365,6 @@ status.
- [Create workflows](#create-workflows).
- [Drag issues between lists](#drag-issues-between-lists).
- [Multi-select issue cards](#multi-select-issue-cards).
-- [Re-order issues in lists](#issue-ordering-in-a-list).
- Drag and reorder the lists.
- Change issue labels (by dragging an issue between lists).
- Close an issue (by dragging it to the **Done** list).
@@ -441,8 +443,9 @@ You can filter by author, assignee, milestone, and label.
### Create workflows
By reordering your lists, you can create workflows. As lists in issue boards are
-based on labels, it works out of the box with your existing issues. So if you've
-already labeled things with 'Backend' and 'Frontend', the issue appears in
+based on labels, it works out of the box with your existing issues.
+
+So if you've already labeled things with **Backend** and **Frontend**, the issue appears in
the lists as you create them. In addition, this means you can easily move
something between lists by changing a label.
@@ -456,20 +459,22 @@ A typical workflow of using an issue board would be:
1. You move issues around in lists so that your team knows who should be working
on what issue.
1. When the work by one team is done, the issue can be dragged to the next list
- so someone else can pick up.
+ so someone else can pick it up.
1. When the issue is finally resolved, the issue is moved to the **Done** list
and gets automatically closed.
-For instance you can create a list based on the label of 'Frontend' and one for
-'Backend'. A designer can start working on an issue by adding it to the
-'Frontend' list. That way, everyone knows that this issue is now being
-worked on by the designers. Then, once they're done, all they have to do is
-drag it over to the next list, 'Backend', where a backend developer can
+For example, you can create a list based on the label of **Frontend** and one for
+**Backend**. A designer can start working on an issue by adding it to the
+**Frontend** list. That way, everyone knows that this issue is now being
+worked on by the designers.
+
+Then, once they're done, all they have to do is
+drag it to the next list, **Backend**, where a backend developer can
eventually pick it up. Once they’re done, they move it to **Done**, to close the
issue.
This process can be seen clearly when visiting an issue since with every move
-to another list the label changes and a system not is recorded.
+to another list the label changes and a system note is recorded.
![issue board system notes](img/issue_board_system_notes.png)
@@ -497,33 +502,6 @@ To select and move multiple cards:
![Multi-select Issue Cards](img/issue_boards_multi_select_v12_4.png)
-### Issue ordering in a list
-
-When visiting a board, issues appear ordered in any list. You're able to change
-that order by dragging and dropping the issues. The changed order will be saved
-to the system so that anybody who visits the same board later will see the reordering,
-with some exceptions.
-
-The first time a given issue appears in any board (that is, the first time a user
-loads a board containing that issue), it is ordered with
-respect to other issues in that list according to [Priority order](labels.md#label-priority).
-
-At that point, that issue is assigned a relative order value by the system
-representing its relative order with respect to the other issues in the list. Any time
-you drag-and-drop reorder that issue, its relative order value changes accordingly.
-
-Also, any time that issue appears in any board when it's loaded by a user,
-the updated relative order value is used for the ordering. (It's only the first
-time an issue appears that it takes from the Priority order mentioned above.) This means that
-if issue `A` is drag-and-drop reordered to be above issue `B` by any user in
-a given board inside your GitLab instance, any time those two issues are subsequently
-loaded in any board in the same instance (could be a different project board or a different group
-board, for example), that ordering is maintained.
-
-This ordering also affects [issue lists](issues/sorting_issue_lists.md).
-Changing the order in an issue board changes the ordering in an issue list,
-and vice versa.
-
## Tips
A few things to remember:
@@ -537,4 +515,4 @@ A few things to remember:
and show only the issues from all lists that have that label.
- For performance and visibility reasons, each list shows the first 20 issues
by default. If you have more than 20 issues, start scrolling down and the next
- 20 appears.
+ 20 appear.
diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md
index f07a5de8141..7ac78108c15 100644
--- a/doc/user/project/issues/design_management.md
+++ b/doc/user/project/issues/design_management.md
@@ -1,3 +1,9 @@
+---
+stage: Create
+group: Knowledge
+info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
+---
+
# Design Management
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/660) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2.
@@ -225,6 +231,40 @@ Note that your resolved comment pins will disappear from the Design to free up s
However, if you need to revisit or find a resolved discussion, all of your resolved threads will be
available in the **Resolved Comment** area at the bottom of the right sidebar.
+## Add To-Do for Designs
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/198439) in GitLab 13.4.
+> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
+> - It's disabled on GitLab.com.
+> - It's not recommended for production use.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-the-design-to-do-button). **(CORE ONLY)**
+
+CAUTION: **Warning:**
+This feature might not be available to you. Check the **version history** note above for details.
+
+Add a To-Do for a design by clicking **Add a To-Do** on the design sidebar:
+
+![To-Do button](img/design_todo_button_v13_4.png)
+
+### Enable or disable the design To-Do button **(CORE ONLY)**
+
+The design To-Do button is under development and not ready for production use. It is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
+can enable it.
+
+To enable it:
+
+```ruby
+Feature.enable(:design_management_todo_button)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:design_management_todo_button)
+```
+
## Referring to designs in Markdown
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217160) in **GitLab 13.1**.
diff --git a/doc/user/project/issues/img/design_todo_button_v13_4.png b/doc/user/project/issues/img/design_todo_button_v13_4.png
new file mode 100644
index 00000000000..62bbecf4ed9
--- /dev/null
+++ b/doc/user/project/issues/img/design_todo_button_v13_4.png
Binary files differ
diff --git a/doc/user/project/issues/sorting_issue_lists.md b/doc/user/project/issues/sorting_issue_lists.md
index 7cbd9906800..8a8359a4b02 100644
--- a/doc/user/project/issues/sorting_issue_lists.md
+++ b/doc/user/project/issues/sorting_issue_lists.md
@@ -11,7 +11,7 @@ etc. The available sorting options can change based on the context of the list.
For sorting by issue priority, see [Label Priority](../labels.md#label-priority).
In group and project issue lists, it is also possible to order issues manually,
-similar to [issue boards](../issue_board.md#issue-ordering-in-a-list).
+similar to [issue boards](../issue_board.md#how-gitlab-orders-issues-in-a-list).
## Manual sorting
@@ -31,6 +31,6 @@ a given list inside your GitLab instance, any time those two issues are subseque
loaded in any list in the same instance (could be a different project issue list or a
different group issue list, for example), that ordering will be maintained.
-This ordering also affects [issue boards](../issue_board.md#issue-ordering-in-a-list).
+This ordering also affects [issue boards](../issue_board.md#how-gitlab-orders-issues-in-a-list).
Changing the order in an issue list changes the ordering in an issue board,
and vice versa.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7f03b9622b0..ba77203154e 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -235,6 +235,7 @@ module API
mount ::API::Templates
mount ::API::Todos
mount ::API::Triggers
+ mount ::API::UsageData
mount ::API::UserCounts
mount ::API::Users
mount ::API::Variables
diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb
new file mode 100644
index 00000000000..05a6f45b21f
--- /dev/null
+++ b/lib/api/usage_data.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module API
+ class UsageData < Grape::API::Instance
+ before { authenticate! }
+
+ namespace 'usage_data' do
+ before do
+ not_found! unless Feature.enabled?(:usage_data_api)
+ end
+
+ desc 'Track usage data events' do
+ detail 'This feature was introduced in GitLab 13.4.'
+ end
+
+ params do
+ requires :event, type: String, desc: 'The event name that should be tracked'
+ end
+
+ post 'increment_unique_users' do
+ event_name = params[:event]
+
+ increment_unique_values(event_name, current_user.id)
+
+ status :ok
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/cleanup/orphan_lfs_file_references.rb b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
index 14eac474e27..10b227f3a2a 100644
--- a/lib/gitlab/cleanup/orphan_lfs_file_references.rb
+++ b/lib/gitlab/cleanup/orphan_lfs_file_references.rb
@@ -17,6 +17,14 @@ module Gitlab
end
def run!
+ # If this project is an LFS storage project (e.g. is the root of a fork
+ # network), what it is safe to remove depends on the sum of its forks.
+ # For now, skip cleaning up LFS for this complicated case
+ if project.forks_count > 0 && project.lfs_storage_project == project
+ log_info("Skipping orphan LFS check for #{project.name_with_namespace} as it is a fork root")
+ return
+ end
+
log_info("Looking for orphan LFS files for project #{project.name_with_namespace}")
remove_orphan_references
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index 5694ce8255a..3626ec5bf5b 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -44,7 +44,10 @@ module Gitlab
# "+ # Test change",
# "- # Old change" ]
def changed_lines(changed_file)
- git.diff_for_file(changed_file).patch.split("\n").select { |line| %r{^[+-]}.match?(line) }
+ diff = git.diff_for_file(changed_file)
+ return [] unless diff
+
+ diff.patch.split("\n").select { |line| %r{^[+-]}.match?(line) }
end
def all_ee_changes
diff --git a/lib/gitlab/logger.rb b/lib/gitlab/logger.rb
index 2ec8c268d09..89a4e36a232 100644
--- a/lib/gitlab/logger.rb
+++ b/lib/gitlab/logger.rb
@@ -32,7 +32,8 @@ module Gitlab
end
def self.build
- Gitlab::SafeRequestStore[self.cache_key] ||= new(self.full_log_path)
+ Gitlab::SafeRequestStore[self.cache_key] ||=
+ new(self.full_log_path, level: ::Logger::DEBUG)
end
def self.full_log_path
diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
index aff3ed53734..6607c73a5c3 100644
--- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb
@@ -91,6 +91,7 @@ module Gitlab
params '%"milestone"'
types Issue, MergeRequest
condition do
+ quick_action_target.supports_milestone? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) &&
find_milestones(project, state: 'active').any?
end
@@ -113,6 +114,7 @@ module Gitlab
condition do
quick_action_target.persisted? &&
quick_action_target.milestone_id? &&
+ quick_action_target.supports_milestone? &&
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
end
command :remove_milestone do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cc0d4638536..c795c9869b5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -872,6 +872,9 @@ msgstr ""
msgid "'%{name}' Value Stream created"
msgstr ""
+msgid "'%{name}' Value Stream deleted"
+msgstr ""
+
msgid "'%{name}' stage already exists"
msgstr ""
@@ -3250,6 +3253,9 @@ msgstr ""
msgid "Are you sure you want to close this blocked issue?"
msgstr ""
+msgid "Are you sure you want to delete \"%{name}\" Value Stream?"
+msgstr ""
+
msgid "Are you sure you want to delete %{name}?"
msgstr ""
@@ -8089,12 +8095,18 @@ msgstr ""
msgid "Delete"
msgstr ""
+msgid "Delete %{name}"
+msgstr ""
+
msgid "Delete Comment"
msgstr ""
msgid "Delete Snippet"
msgstr ""
+msgid "Delete Value Stream"
+msgstr ""
+
msgid "Delete account"
msgstr ""
@@ -22112,6 +22124,9 @@ msgstr ""
msgid "SecurityConfiguration|Using custom settings. You won't receive automatic updates on this variable. %{anchorStart}Restore to default%{anchorEnd}"
msgstr ""
+msgid "SecurityConfiguration|View history"
+msgstr ""
+
msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}."
msgstr ""
diff --git a/package.json b/package.json
index 31746279459..8cf19efed07 100644
--- a/package.json
+++ b/package.json
@@ -92,6 +92,7 @@
"glob": "^7.1.6",
"graphql": "^14.7.0",
"graphql-tag": "^2.10.1",
+ "gray-matter": "^4.0.2",
"immer": "^7.0.7",
"imports-loader": "^0.8.0",
"ipaddr.js": "^1.9.1",
diff --git a/spec/factories/instance_statistics/measurement.rb b/spec/factories/instance_statistics/measurement.rb
index 6c367251dc6..fb180c23214 100644
--- a/spec/factories/instance_statistics/measurement.rb
+++ b/spec/factories/instance_statistics/measurement.rb
@@ -3,7 +3,15 @@
FactoryBot.define do
factory :instance_statistics_measurement, class: 'Analytics::InstanceStatistics::Measurement' do
recorded_at { Time.now }
- identifier { Analytics::InstanceStatistics::Measurement.identifiers[:projects] }
+ identifier { :projects }
count { 1_000 }
+
+ trait :project_count do
+ identifier { :projects }
+ end
+
+ trait :group_count do
+ identifier { :groups }
+ end
end
end
diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb
index d7c9c8bddb1..3d18aef9327 100644
--- a/spec/features/merge_request/user_accepts_merge_request_spec.rb
+++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb
@@ -12,12 +12,38 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
sign_in(user)
end
- it 'presents merged merge request content' do
- visit(merge_request_path(merge_request))
+ context 'presents merged merge request content' do
+ it 'when merge method is set to merge commit' do
+ visit(merge_request_path(merge_request))
+
+ click_button('Merge')
+
+ expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
+ end
+
+ context 'when merge method is set to fast-forward merge' do
+ let(:project) { create(:project, :public, :repository, merge_requests_ff_only_enabled: true) }
+
+ it 'accepts a merge request with rebase and merge' do
+ merge_request = create(:merge_request, :rebased, source_project: project)
+
+ visit(merge_request_path(merge_request))
- click_button('Merge')
+ click_button('Merge')
- expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merge_commit_sha}")
+ expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
+ end
+
+ it 'accepts a merge request with squash and merge' do
+ merge_request = create(:merge_request, :rebased, source_project: project, squash: true)
+
+ visit(merge_request_path(merge_request))
+
+ click_button('Merge')
+
+ expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
+ end
+ end
end
context 'with removing the source branch' do
diff --git a/spec/fixtures/api/schemas/entities/merge_request_poll_cached_widget.json b/spec/fixtures/api/schemas/entities/merge_request_poll_cached_widget.json
index b40b71d2cd6..3d6b20896ce 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_poll_cached_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_poll_cached_widget.json
@@ -7,8 +7,8 @@
"title": { "type": "string" },
"auto_merge_enabled": { "type": "boolean" },
"state": { "type": "string" },
- "merge_commit_sha": { "type": ["string", "null"] },
- "short_merge_commit_sha": { "type": ["string", "null"] },
+ "merged_commit_sha": { "type": ["string", "null"] },
+ "short_merged_commit_sha": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
"merge_status": { "type": "string" },
"merge_user_id": { "type": ["integer", "null"] },
@@ -40,7 +40,7 @@
"diverged_commits_count": { "type": "integer" },
"target_branch_commits_path": { "type": "string" },
"target_branch_tree_path": { "type": "string" },
- "merge_commit_path": { "type": ["string", "null"] },
+ "merged_commit_path": { "type": ["string", "null"] },
"source_branch_with_namespace_link": { "type": "string" },
"source_branch_path": { "type": "string" }
}
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index f86091d427c..e2e7af624f8 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -1,19 +1,24 @@
import { shallowMount } from '@vue/test-utils';
import { GlBanner } from '@gitlab/ui';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
+import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
-const expectedTitle = 'Collaborate with your team';
-const expectedBody =
+jest.mock('~/lib/utils/common_utils');
+
+const isDismissedKey = 'invite_99_1';
+const title = 'Collaborate with your team';
+const body =
"We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge";
-const expectedSvgPath = '/illustrations/background';
-const expectedInviteMembersPath = 'groups/members';
-const expectedButtonText = 'Invite your colleagues';
+const svgPath = '/illustrations/background';
+const inviteMembersPath = 'groups/members';
+const buttonText = 'Invite your colleagues';
const createComponent = (stubs = {}) => {
return shallowMount(InviteMembersBanner, {
provide: {
- svgPath: expectedSvgPath,
- inviteMembersPath: expectedInviteMembersPath,
+ svgPath,
+ inviteMembersPath,
+ isDismissedKey,
},
stubs,
});
@@ -37,23 +42,23 @@ describe('InviteMembersBanner', () => {
});
it('uses the svgPath for the banner svgpath', () => {
- expect(findBanner().attributes('svgpath')).toBe(expectedSvgPath);
+ expect(findBanner().attributes('svgpath')).toBe(svgPath);
});
it('uses the title from options for title', () => {
- expect(findBanner().attributes('title')).toBe(expectedTitle);
+ expect(findBanner().attributes('title')).toBe(title);
});
it('includes the body text from options', () => {
- expect(findBanner().html()).toContain(expectedBody);
+ expect(findBanner().html()).toContain(body);
});
it('uses the button_text text from options for buttontext', () => {
- expect(findBanner().attributes('buttontext')).toBe(expectedButtonText);
+ expect(findBanner().attributes('buttontext')).toBe(buttonText);
});
it('uses the href from inviteMembersPath for buttonlink', () => {
- expect(findBanner().attributes('buttonlink')).toBe(expectedInviteMembersPath);
+ expect(findBanner().attributes('buttonlink')).toBe(inviteMembersPath);
});
});
@@ -61,16 +66,35 @@ describe('InviteMembersBanner', () => {
const findButton = () => {
return wrapper.find('button');
};
- const stubs = {
- GlBanner,
- };
- it('sets visible to false', () => {
- wrapper = createComponent(stubs);
+ beforeEach(() => {
+ wrapper = createComponent({ GlBanner });
findButton().trigger('click');
+ });
+
+ it('sets iDismissed to true', () => {
+ expect(wrapper.vm.isDismissed).toBe(true);
+ });
+
+ it('sets the cookie with the isDismissedKey', () => {
+ expect(setCookie).toHaveBeenCalledWith(isDismissedKey, true);
+ });
+ });
+
+ describe('when a dismiss cookie exists', () => {
+ beforeEach(() => {
+ parseBoolean.mockReturnValue(true);
+
+ wrapper = createComponent({ GlBanner });
+ });
+
+ it('sets isDismissed to true', () => {
+ expect(wrapper.vm.isDismissed).toBe(true);
+ });
- expect(wrapper.vm.visible).toBe(false);
+ it('does not render the banner', () => {
+ expect(wrapper.contains(GlBanner)).toBe(false);
});
});
});
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
index 33c2f641ada..83599ec00c4 100644
--- a/spec/frontend/static_site_editor/components/edit_area_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_area_spec.js
@@ -81,7 +81,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
it('updates parsedSource with new content', () => {
const newContent = 'New content';
- const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'sync');
+ const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'syncContent');
findRichContentEditor().vm.$emit('input', newContent);
diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js
index 29a6fa23d2f..d861f6c9cd7 100644
--- a/spec/frontend/static_site_editor/mock_data.js
+++ b/spec/frontend/static_site_editor/mock_data.js
@@ -1,31 +1,22 @@
export const sourceContentHeaderYAML = `---
layout: handbook-page-toc
title: Handbook
-twitter_image: '/images/tweets/handbook-gitlab.png'
+twitter_image: /images/tweets/handbook-gitlab.png
---`;
-export const sourceContentHeaderTOML = `+++
-layout: "handbook-page-toc"
-title: "Handbook"
-twitter_image: "/images/tweets/handbook-gitlab.png"
-+++`;
-export const sourceContentHeaderJSON = `{
-"layout": "handbook-page-toc",
-"title": "Handbook",
-"twitter_image": "/images/tweets/handbook-gitlab.png",
-}`;
-export const sourceContentSpacing = `
-`;
+export const sourceContentHeaderObjYAML = {
+ layout: 'handbook-page-toc',
+ title: 'Handbook',
+ twitter_image: '/images/tweets/handbook-gitlab.png',
+};
+export const sourceContentSpacing = `\n`;
export const sourceContentBody = `## On this page
{:.no_toc .hidden-md .hidden-lg}
- TOC
{:toc .hidden-md .hidden-lg}
-![image](path/to/image1.png)
-`;
+![image](path/to/image1.png)`;
export const sourceContentYAML = `${sourceContentHeaderYAML}${sourceContentSpacing}${sourceContentBody}`;
-export const sourceContentTOML = `${sourceContentHeaderTOML}${sourceContentSpacing}${sourceContentBody}`;
-export const sourceContentJSON = `${sourceContentHeaderJSON}${sourceContentSpacing}${sourceContentBody}`;
export const sourceContentTitle = 'Handbook';
export const username = 'gitlabuser';
diff --git a/spec/frontend/static_site_editor/services/parse_source_file_language_support_spec.js b/spec/frontend/static_site_editor/services/parse_source_file_language_support_spec.js
deleted file mode 100644
index 9bc706c31d6..00000000000
--- a/spec/frontend/static_site_editor/services/parse_source_file_language_support_spec.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import getFrontMatterLanguageDefinition from '~/static_site_editor/services/parse_source_file_language_support';
-
-describe('static_site_editor/services/parse_source_file_language_support', () => {
- describe('getFrontMatterLanguageDefinition', () => {
- it.each`
- languageName
- ${'yaml'}
- ${'toml'}
- ${'json'}
- ${'abcd'}
- `('returns $hasMatch when provided $languageName', ({ languageName }) => {
- try {
- const definition = getFrontMatterLanguageDefinition(languageName);
- expect(definition.name).toBe(languageName);
- } catch (error) {
- expect(error.message).toBe(`Unsupported front matter language: ${languageName}`);
- }
- });
- });
-});
diff --git a/spec/frontend/static_site_editor/services/parse_source_file_spec.js b/spec/frontend/static_site_editor/services/parse_source_file_spec.js
index 6d55bed6721..ba68741f255 100644
--- a/spec/frontend/static_site_editor/services/parse_source_file_spec.js
+++ b/spec/frontend/static_site_editor/services/parse_source_file_spec.js
@@ -1,10 +1,7 @@
import {
sourceContentYAML as content,
- sourceContentTOML as tomlContent,
- sourceContentJSON as jsonContent,
sourceContentHeaderYAML as yamlFrontMatter,
- sourceContentHeaderTOML as tomlFrontMatter,
- sourceContentHeaderJSON as jsonFrontMatter,
+ sourceContentHeaderObjYAML as yamlFrontMatterObj,
sourceContentBody as body,
} from '../mock_data';
@@ -18,20 +15,15 @@ describe('static_site_editor/services/parse_source_file', () => {
const newContentComplex = `${contentComplex} ${edit}`;
describe('unmodified front matter', () => {
- const yamlOptions = { frontMatterLanguage: 'yaml' };
-
it.each`
- parsedSource | targetFrontMatter
- ${parseSourceFile(content)} | ${yamlFrontMatter}
- ${parseSourceFile(contentComplex)} | ${yamlFrontMatter}
- ${parseSourceFile(content, yamlOptions)} | ${yamlFrontMatter}
- ${parseSourceFile(contentComplex, yamlOptions)} | ${yamlFrontMatter}
- ${parseSourceFile(tomlContent, { frontMatterLanguage: 'toml' })} | ${tomlFrontMatter}
- ${parseSourceFile(jsonContent, { frontMatterLanguage: 'json' })} | ${jsonFrontMatter}
+ parsedSource | targetFrontMatter
+ ${parseSourceFile(content)} | ${yamlFrontMatter}
+ ${parseSourceFile(contentComplex)} | ${yamlFrontMatter}
`(
'returns $targetFrontMatter when frontMatter queried',
({ parsedSource, targetFrontMatter }) => {
- expect(parsedSource.frontMatter()).toBe(targetFrontMatter);
+ expect(targetFrontMatter).toContain(parsedSource.matter());
+ expect(parsedSource.matterObject()).toEqual(yamlFrontMatterObj);
},
);
});
@@ -63,6 +55,7 @@ describe('static_site_editor/services/parse_source_file', () => {
describe('modified front matter', () => {
const newYamlFrontMatter = '---\nnewKey: newVal\n---';
+ const newYamlFrontMatterObj = { newKey: 'newVal' };
const contentWithNewFrontMatter = content.replace(yamlFrontMatter, newYamlFrontMatter);
const contentComplexWithNewFrontMatter = contentComplex.replace(
yamlFrontMatter,
@@ -76,11 +69,12 @@ describe('static_site_editor/services/parse_source_file', () => {
`(
'returns the correct front matter and modified content',
({ parsedSource, targetContent }) => {
- expect(parsedSource.frontMatter()).toBe(yamlFrontMatter);
+ expect(yamlFrontMatter).toContain(parsedSource.matter());
- parsedSource.setFrontMatter(newYamlFrontMatter);
+ parsedSource.syncMatter(newYamlFrontMatter);
- expect(parsedSource.frontMatter()).toBe(newYamlFrontMatter);
+ expect(parsedSource.matter()).toBe(newYamlFrontMatter);
+ expect(parsedSource.matterObject()).toEqual(newYamlFrontMatterObj);
expect(parsedSource.content()).toBe(targetContent);
},
);
@@ -99,7 +93,7 @@ describe('static_site_editor/services/parse_source_file', () => {
`(
'returns $isModified after a $targetRaw sync',
({ parsedSource, isModified, targetRaw, targetBody }) => {
- parsedSource.sync(targetRaw);
+ parsedSource.syncContent(targetRaw);
expect(parsedSource.isModified()).toBe(isModified);
expect(parsedSource.content()).toBe(targetRaw);
diff --git a/spec/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver_spec.rb b/spec/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver_spec.rb
new file mode 100644
index 00000000000..76854be2daa
--- /dev/null
+++ b/spec/graphql/resolvers/admin/analytics/instance_statistics/measurements_resolver_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin_user) { create(:user, :admin) }
+
+ let_it_be(:project_measurement_new) { create(:instance_statistics_measurement, :project_count, recorded_at: 2.days.ago) }
+ let_it_be(:project_measurement_old) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago) }
+
+ subject { resolve_measurements({ identifier: 'projects' }, { current_user: current_user }) }
+
+ context 'when requesting project count measurements' do
+ context 'as an admin user' do
+ let(:current_user) { admin_user }
+
+ it 'returns the records, latest first' do
+ expect(subject).to eq([project_measurement_new, project_measurement_old])
+ end
+ end
+
+ context 'as a non-admin user' do
+ let(:current_user) { user }
+
+ it 'raises ResourceNotAvailable error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'as an unauthenticated user' do
+ let(:current_user) { nil }
+
+ it 'raises ResourceNotAvailable error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+ end
+ end
+
+ def resolve_measurements(args = {}, context = {})
+ resolve(described_class, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum_spec.rb b/spec/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum_spec.rb
new file mode 100644
index 00000000000..625fb17bbf8
--- /dev/null
+++ b/spec/graphql/types/admin/analytics/instance_statistics/measurement_identifier_enum_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['MeasurementIdentifier'] do
+ specify { expect(described_class.graphql_name).to eq('MeasurementIdentifier') }
+
+ it 'exposes all the existing identifier values' do
+ identifiers = Analytics::InstanceStatistics::Measurement.identifiers.keys.map(&:upcase)
+
+ expect(described_class.values.keys).to match_array(identifiers)
+ end
+end
diff --git a/spec/graphql/types/admin/analytics/instance_statistics/measurement_type_spec.rb b/spec/graphql/types/admin/analytics/instance_statistics/measurement_type_spec.rb
new file mode 100644
index 00000000000..de8143a5466
--- /dev/null
+++ b/spec/graphql/types/admin/analytics/instance_statistics/measurement_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['InstanceStatisticsMeasurement'] do
+ subject { described_class }
+
+ it { is_expected.to have_graphql_field(:recorded_at) }
+ it { is_expected.to have_graphql_field(:identifier) }
+ it { is_expected.to have_graphql_field(:count) }
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index d09fd34b551..11f780a4f3f 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -21,6 +21,7 @@ RSpec.describe GitlabSchema.types['Query'] do
user
users
issue
+ instance_statistics_measurements
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
@@ -62,4 +63,12 @@ RSpec.describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_type(Types::IssueType)
end
end
+
+ describe 'instance_statistics_measurements field' do
+ subject { described_class.fields['instanceStatisticsMeasurements'] }
+
+ it 'returns issue' do
+ is_expected.to have_graphql_type(Types::Admin::Analytics::InstanceStatistics::MeasurementType.connection_type)
+ end
+ end
end
diff --git a/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb b/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb
index 507440064bc..b120f77d5f6 100644
--- a/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb
+++ b/spec/lib/gitlab/cleanup/orphan_lfs_file_references_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Cleanup::OrphanLfsFileReferences do
+ include ProjectForksHelper
+
let(:null_logger) { Logger.new('/dev/null') }
let(:project) { create(:project, :repository, lfs_enabled: true) }
let(:lfs_object) { create(:lfs_object) }
@@ -85,4 +87,42 @@ RSpec.describe Gitlab::Cleanup::OrphanLfsFileReferences do
.to receive(:get_all_lfs_pointers)
.and_return(oids.map { |oid| OpenStruct.new(lfs_oid: oid) })
end
+
+ context 'LFS for forked projects' do
+ let!(:fork_root) { create(:project, :repository, lfs_enabled: true) }
+ let!(:fork_internal) { fork_project(fork_root, nil, repository: true) }
+ let!(:fork_leaf) { fork_project(fork_internal, nil, repository: true) }
+
+ let(:dry_run) { true }
+
+ context 'root node' do
+ let(:project) { fork_root }
+
+ it 'skips cleanup' do
+ expect(service).not_to receive(:remove_orphan_references)
+
+ service.run!
+ end
+ end
+
+ context 'internal node' do
+ let(:project) { fork_internal }
+
+ it 'runs cleanup' do
+ expect(service).to receive(:remove_orphan_references)
+
+ service.run!
+ end
+ end
+
+ context 'leaf node' do
+ let(:project) { fork_leaf }
+
+ it 'runs cleanup' do
+ expect(service).to receive(:remove_orphan_references)
+
+ service.run!
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 0e66bf8691e..c7d55c396ef 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -76,6 +76,30 @@ RSpec.describe Gitlab::Danger::Helper do
end
end
+ describe '#changed_lines' do
+ subject { helper.changed_lines('changed_file.rb') }
+
+ before do
+ allow(fake_git).to receive(:diff_for_file).with('changed_file.rb').and_return(diff)
+ end
+
+ context 'when file has diff' do
+ let(:diff) { double(:diff, patch: "+ # New change here\n+ # New change there") }
+
+ it 'returns file changes' do
+ is_expected.to eq(['+ # New change here', '+ # New change there'])
+ end
+ end
+
+ context 'when file has no diff (renamed without changes)' do
+ let(:diff) { nil }
+
+ it 'returns a blank array' do
+ is_expected.to eq([])
+ end
+ end
+ end
+
describe "changed_files" do
it 'returns list of changed files matching given regex' do
expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb usage_data.rb])
diff --git a/spec/migrations/complete_namespace_settings_migration_spec.rb b/spec/migrations/complete_namespace_settings_migration_spec.rb
new file mode 100644
index 00000000000..7820536f355
--- /dev/null
+++ b/spec/migrations/complete_namespace_settings_migration_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200907124300_complete_namespace_settings_migration.rb')
+
+RSpec.describe CompleteNamespaceSettingsMigration, :redis do
+ let(:migration) { spy('migration') }
+
+ context 'when still legacy artifacts exist' do
+ let(:namespaces) { table(:namespaces) }
+ let(:namespace_settings) { table(:namespace_settings) }
+ let!(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
+
+ it 'steals sidekiq jobs from BackfillNamespaceSettings background migration' do
+ expect(Gitlab::BackgroundMigration).to receive(:steal).with('BackfillNamespaceSettings')
+
+ migrate!
+ end
+
+ it 'migrates namespaces without namespace_settings' do
+ expect { migrate! }.to change { namespace_settings.count }.from(0).to(1)
+ end
+ end
+end
diff --git a/spec/models/analytics/instance_statistics/measurement_spec.rb b/spec/models/analytics/instance_statistics/measurement_spec.rb
index 64a5229b854..4df847ea524 100644
--- a/spec/models/analytics/instance_statistics/measurement_spec.rb
+++ b/spec/models/analytics/instance_statistics/measurement_spec.rb
@@ -11,4 +11,35 @@ RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do
it { is_expected.to validate_presence_of(:count) }
it { is_expected.to validate_uniqueness_of(:recorded_at).scoped_to(:identifier) }
end
+
+ describe 'identifiers enum' do
+ it 'maps to the correct values' do
+ expect(described_class.identifiers).to eq({
+ projects: 1,
+ users: 2,
+ issues: 3,
+ merge_requests: 4,
+ groups: 5,
+ pipelines: 6
+ }.with_indifferent_access)
+ end
+ end
+
+ describe 'scopes' do
+ let_it_be(:measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago) }
+ let_it_be(:measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 2.days.ago) }
+ let_it_be(:measurement_3) { create(:instance_statistics_measurement, :group_count, recorded_at: 5.days.ago) }
+
+ describe '.order_by_latest' do
+ subject { described_class.order_by_latest }
+
+ it { is_expected.to eq([measurement_2, measurement_3, measurement_1]) }
+ end
+
+ describe '.with_identifier' do
+ subject { described_class.with_identifier(:projects) }
+
+ it { is_expected.to match_array([measurement_1, measurement_2]) }
+ end
+ end
end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index d9ab326505b..5ea1907543a 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -30,49 +30,51 @@ RSpec.describe ApplicationRecord do
end
end
- describe '.safe_find_or_create_by' do
- it 'creates the user avoiding race conditions' do
- expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
- allow(Suggestion).to receive(:find_or_create_by).and_call_original
+ context 'safe find or create methods' do
+ let_it_be(:note) { create(:diff_note_on_merge_request) }
- expect { Suggestion.safe_find_or_create_by(build(:suggestion).attributes) }
- .to change { Suggestion.count }.by(1)
- end
+ let(:suggestion_attributes) { attributes_for(:suggestion).merge!(note_id: note.id) }
- it 'passes a block to find_or_create_by' do
- attributes = build(:suggestion).attributes
+ describe '.safe_find_or_create_by' do
+ it 'creates the suggestion avoiding race conditions' do
+ expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
+ allow(Suggestion).to receive(:find_or_create_by).and_call_original
- expect do |block|
- Suggestion.safe_find_or_create_by(attributes, &block)
- end.to yield_with_args(an_object_having_attributes(attributes))
- end
+ expect { Suggestion.safe_find_or_create_by(suggestion_attributes) }
+ .to change { Suggestion.count }.by(1)
+ end
- it 'does not create a record when is not valid' do
- raw_usage_data = RawUsageData.safe_find_or_create_by({ recorded_at: nil })
+ it 'passes a block to find_or_create_by' do
+ expect do |block|
+ Suggestion.safe_find_or_create_by(suggestion_attributes, &block)
+ end.to yield_with_args(an_object_having_attributes(suggestion_attributes))
+ end
- expect(raw_usage_data.id).to be_nil
- expect(raw_usage_data).not_to be_valid
- end
- end
+ it 'does not create a record when is not valid' do
+ raw_usage_data = RawUsageData.safe_find_or_create_by({ recorded_at: nil })
- describe '.safe_find_or_create_by!' do
- it 'creates a record using safe_find_or_create_by' do
- expect(Suggestion).to receive(:find_or_create_by).and_call_original
-
- expect(Suggestion.safe_find_or_create_by!(build(:suggestion).attributes))
- .to be_a(Suggestion)
+ expect(raw_usage_data.id).to be_nil
+ expect(raw_usage_data).not_to be_valid
+ end
end
- it 'raises a validation error if the record was not persisted' do
- expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
- end
+ describe '.safe_find_or_create_by!' do
+ it 'creates a record using safe_find_or_create_by' do
+ expect(Suggestion).to receive(:find_or_create_by).and_call_original
+
+ expect(Suggestion.safe_find_or_create_by!(suggestion_attributes))
+ .to be_a(Suggestion)
+ end
- it 'passes a block to find_or_create_by' do
- attributes = build(:suggestion).attributes
+ it 'raises a validation error if the record was not persisted' do
+ expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
+ end
- expect do |block|
- Suggestion.safe_find_or_create_by!(attributes, &block)
- end.to yield_with_args(an_object_having_attributes(attributes))
+ it 'passes a block to find_or_create_by' do
+ expect do |block|
+ Suggestion.safe_find_or_create_by!(suggestion_attributes, &block)
+ end.to yield_with_args(an_object_having_attributes(suggestion_attributes))
+ end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 3563bbb2867..cbc54b5d20f 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2160,6 +2160,60 @@ RSpec.describe MergeRequest, factory_default: :keep do
end
end
+ describe '#merged_commit_sha' do
+ it 'returns nil when not merged' do
+ expect(subject.merged_commit_sha).to be_nil
+ end
+
+ context 'when the MR is merged' do
+ let(:sha) { 'f7ce827c314c9340b075657fd61c789fb01cf74d' }
+
+ before do
+ subject.mark_as_merged!
+ end
+
+ it 'returns merge_commit_sha when there is a merge_commit_sha' do
+ subject.update_attribute(:merge_commit_sha, sha)
+
+ expect(subject.merged_commit_sha).to eq(sha)
+ end
+
+ it 'returns squash_commit_sha when there is a squash_commit_sha' do
+ subject.update_attribute(:squash_commit_sha, sha)
+
+ expect(subject.merged_commit_sha).to eq(sha)
+ end
+
+ it 'returns diff_head_sha when there are no merge_commit_sha and squash_commit_sha' do
+ allow(subject).to receive(:diff_head_sha).and_return(sha)
+
+ expect(subject.merged_commit_sha).to eq(sha)
+ end
+ end
+ end
+
+ describe '#short_merged_commit_sha' do
+ context 'when merged_commit_sha is nil' do
+ before do
+ allow(subject).to receive(:merged_commit_sha).and_return(nil)
+ end
+
+ it 'returns nil' do
+ expect(subject.short_merged_commit_sha).to be_nil
+ end
+ end
+
+ context 'when merged_commit_sha is present' do
+ before do
+ allow(subject).to receive(:merged_commit_sha).and_return('f7ce827c314c9340b075657fd61c789fb01cf74d')
+ end
+
+ it 'returns shortened merged_commit_sha' do
+ expect(subject.short_merged_commit_sha).to eq('f7ce827c')
+ end
+ end
+ end
+
describe '#can_be_reverted?' do
subject { create(:merge_request, source_project: create(:project, :repository)) }
diff --git a/spec/requests/api/graphql/instance_statistics_measurements_spec.rb b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
new file mode 100644
index 00000000000..b8cbe54534a
--- /dev/null
+++ b/spec/requests/api/graphql/instance_statistics_measurements_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'InstanceStatisticsMeasurements' do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user, :admin) }
+ let!(:instance_statistics_measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 20.days.ago, count: 5) }
+ let!(:instance_statistics_measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago, count: 10) }
+
+ let(:query) { graphql_query_for(:instanceStatisticsMeasurements, 'identifier: PROJECTS', 'nodes { count }') }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it 'returns measurement objects' do
+ expect(graphql_data.dig('instanceStatisticsMeasurements', 'nodes')).to eq([{ "count" => 10 }, { "count" => 5 }])
+ end
+end
diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb
new file mode 100644
index 00000000000..6372758bdde
--- /dev/null
+++ b/spec/requests/api/usage_data_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::UsageData do
+ let_it_be(:user) { create(:user) }
+
+ describe 'POST /usage_data/increment_unique_users' do
+ let(:endpoint) { '/usage_data/increment_unique_users' }
+ let(:known_event) { 'g_compliance_dashboard' }
+ let(:unknown_event) { 'unknown' }
+
+ context 'usage_data_api feature not enabled' do
+ it 'returns not_found' do
+ stub_feature_flags(usage_data_api: false)
+
+ post api(endpoint, user), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'without authentication' do
+ it 'returns 401 response' do
+ post api(endpoint), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'with authentication' do
+ before do
+ stub_feature_flags(usage_data_api: true)
+ stub_feature_flags("usage_data_#{known_event}" => true)
+ stub_application_setting(usage_ping_enabled: true)
+ end
+
+ context 'when event is missing from params' do
+ it 'returns bad request' do
+ post api(endpoint, user), params: {}
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'with correct params' do
+ it 'returns status ok' do
+ expect(Gitlab::Redis::HLL).to receive(:add)
+
+ post api(endpoint, user), params: { event: known_event }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with unknown event' do
+ it 'returns status ok' do
+ expect(Gitlab::Redis::HLL).not_to receive(:add)
+
+ post api(endpoint, user), params: { event: unknown_event }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index 3c02b56f1a5..64aa845841b 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -30,10 +30,9 @@ RSpec.describe Notes::QuickActionsService do
end
it 'closes noteable, sets labels, assigns, and sets milestone to noteable, and leave no note' do
- content, update_params = service.execute(note)
- service.apply_updates(update_params, note)
+ content = execute(note)
- expect(content).to eq ''
+ expect(content).to be_empty
expect(note.noteable).to be_closed
expect(note.noteable.labels).to match_array(labels)
expect(note.noteable.assignees).to eq([assignee])
@@ -54,19 +53,13 @@ RSpec.describe Notes::QuickActionsService do
end
it 'does not create issue relation' do
- expect do
- _, update_params = service.execute(note)
- service.apply_updates(update_params, note)
- end.not_to change { IssueLink.count }
+ expect { execute(note) }.not_to change { IssueLink.count }
end
end
context 'user is allowed to relate issues' do
it 'creates issue relation' do
- expect do
- _, update_params = service.execute(note)
- service.apply_updates(update_params, note)
- end.to change { IssueLink.count }.by(1)
+ expect { execute(note) }.to change { IssueLink.count }.by(1)
end
end
end
@@ -79,10 +72,9 @@ RSpec.describe Notes::QuickActionsService do
let(:note_text) { '/reopen' }
it 'opens the noteable, and leave no note' do
- content, update_params = service.execute(note)
- service.apply_updates(update_params, note)
+ content = execute(note)
- expect(content).to eq ''
+ expect(content).to be_empty
expect(note.noteable).to be_open
end
end
@@ -92,10 +84,9 @@ RSpec.describe Notes::QuickActionsService do
let(:note_text) { '/spend 1h' }
it 'adds time to noteable, adds timelog with nil note_id and has no content' do
- content, update_params = service.execute(note)
- service.apply_updates(update_params, note)
+ content = execute(note)
- expect(content).to eq ''
+ expect(content).to be_empty
expect(note.noteable.time_spent).to eq(3600)
expect(Timelog.last.note_id).to be_nil
end
@@ -122,8 +113,7 @@ RSpec.describe Notes::QuickActionsService do
end
it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
- content, update_params = service.execute(note)
- service.apply_updates(update_params, note)
+ content = execute(note)
expect(content).to eq "HELLO\nWORLD"
expect(note.noteable).to be_closed
@@ -141,14 +131,87 @@ RSpec.describe Notes::QuickActionsService do
let(:note_text) { "HELLO\n/reopen\nWORLD" }
it 'opens the noteable' do
- content, update_params = service.execute(note)
- service.apply_updates(update_params, note)
+ content = execute(note)
expect(content).to eq "HELLO\nWORLD"
expect(note.noteable).to be_open
end
end
end
+
+ describe '/milestone' do
+ let(:issue) { create(:issue, project: project) }
+ let(:note_text) { %(/milestone %"#{milestone.name}") }
+ let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
+
+ context 'on an incident' do
+ before do
+ issue.update!(issue_type: :incident)
+ end
+
+ it 'leaves the note empty' do
+ expect(execute(note)).to be_empty
+ end
+
+ it 'does not assign the milestone' do
+ expect { execute(note) }.not_to change { issue.reload.milestone }
+ end
+ end
+
+ context 'on a merge request' do
+ let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }
+
+ it 'leaves the note empty' do
+ expect(execute(note_mr)).to be_empty
+ end
+
+ it 'assigns the milestone' do
+ expect { execute(note) }.to change { issue.reload.milestone }.from(nil).to(milestone)
+ end
+ end
+ end
+
+ describe '/remove_milestone' do
+ let(:issue) { create(:issue, project: project, milestone: milestone) }
+ let(:note_text) { '/remove_milestone' }
+ let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
+
+ context 'on an issue' do
+ it 'leaves the note empty' do
+ expect(execute(note)).to be_empty
+ end
+
+ it 'removes the milestone' do
+ expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
+ end
+ end
+
+ context 'on an incident' do
+ before do
+ issue.update!(issue_type: :incident)
+ end
+
+ it 'leaves the note empty' do
+ expect(execute(note)).to be_empty
+ end
+
+ it 'does not remove the milestone' do
+ expect { execute(note) }.not_to change { issue.reload.milestone }
+ end
+ end
+
+ context 'on a merge request' do
+ let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }
+
+ it 'leaves the note empty' do
+ expect(execute(note_mr)).to be_empty
+ end
+
+ it 'removes the milestone' do
+ expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
+ end
+ end
+ end
end
describe '.noteable_update_service' do
@@ -215,7 +278,7 @@ RSpec.describe Notes::QuickActionsService do
end
it_behaves_like 'note on noteable that supports quick actions' do
- let_it_be(:merge_request, reload: true) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
let(:note) { build(:note_on_merge_request, project: project, noteable: merge_request) }
end
end
@@ -239,11 +302,17 @@ RSpec.describe Notes::QuickActionsService do
end
it 'adds only one assignee from the list' do
- _, update_params = service.execute(note)
- service.apply_updates(update_params, note)
+ execute(note)
expect(note.noteable.assignees.count).to eq(1)
end
end
end
+
+ def execute(note)
+ content, update_params = service.execute(note)
+ service.apply_updates(update_params, note)
+
+ content
+ end
end
diff --git a/yarn.lock b/yarn.lock
index 7c160a211fd..c3ddc6fce32 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5581,6 +5581,16 @@ graphql@^14.7.0:
dependencies:
iterall "^1.2.2"
+gray-matter@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.2.tgz#9aa379e3acaf421193fce7d2a28cebd4518ac454"
+ integrity sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==
+ dependencies:
+ js-yaml "^3.11.0"
+ kind-of "^6.0.2"
+ section-matter "^1.0.0"
+ strip-bom-string "^1.0.0"
+
growly@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
@@ -7106,7 +7116,7 @@ js-cookie@^2.2.1:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-js-yaml@^3.12.0, js-yaml@^3.13.1, js-yaml@~3.13.1:
+js-yaml@^3.11.0, js-yaml@^3.12.0, js-yaml@^3.13.1, js-yaml@~3.13.1:
version "3.13.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
@@ -10492,6 +10502,14 @@ scss-tokenizer@^0.2.3:
js-base64 "^2.1.8"
source-map "^0.4.2"
+section-matter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"
+ integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==
+ dependencies:
+ extend-shallow "^2.0.1"
+ kind-of "^6.0.0"
+
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -11154,6 +11172,11 @@ strip-ansi@^6.0.0:
dependencies:
ansi-regex "^5.0.0"
+strip-bom-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"
+ integrity sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=
+
strip-bom@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"