diff options
56 files changed, 728 insertions, 184 deletions
diff --git a/.gitlab/ci/workhorse.gitlab-ci.yml b/.gitlab/ci/workhorse.gitlab-ci.yml index 0da0a334699..47b5d29ab8e 100644 --- a/.gitlab/ci/workhorse.gitlab-ci.yml +++ b/.gitlab/ci/workhorse.gitlab-ci.yml @@ -9,24 +9,21 @@ workhorse:verify: .workhorse:test: extends: .workhorse:rules:workhorse - services: - - name: registry.gitlab.com/gitlab-org/build/cng/gitaly:latest - # Disable the hooks so we don't have to stub the GitLab API - command: ["/usr/bin/env", "GITALY_TESTING_NO_GIT_HOOKS=1", "/scripts/process-wrapper"] - alias: gitaly variables: - GITALY_ADDRESS: "tcp://gitaly:8075" + GITALY_ADDRESS: "tcp://127.0.0.1:8075" stage: test - needs: [] + needs: + - setup-test-env script: - go version - apt-get update && apt-get -y install libimage-exiftool-perl + - scripts/gitaly-test-build - make -C workhorse test workhorse:test using go 1.16: extends: .workhorse:test - image: ${GITLAB_DEPENDENCY_PROXY}golang:1.16 + image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7-golang-1.16-git-2.31 workhorse:test using go 1.17: extends: .workhorse:test - image: ${GITLAB_DEPENDENCY_PROXY}golang:1.17 + image: registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.7-golang-1.17-git-2.31 diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index 5092c30aa60..42d5d8fb359 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -4,7 +4,7 @@ import VueDraggable from 'vuedraggable'; import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql'; import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql'; import createFlash, { FLASH_TYPES } from '~/flash'; -import { getFilename } from '~/lib/utils/file_upload'; +import { getFilename, validateImageName } from '~/lib/utils/file_upload'; import { __, s__, sprintf } from '~/locale'; import DesignDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue'; import DeleteButton from '../components/delete_button.vue'; @@ -284,12 +284,16 @@ export default { return; } event.preventDefault(); - let filename = getFilename(event); - if (!filename || filename === 'image.png') { - filename = `design_${Date.now()}.png`; - } - const newFile = new File([files[0]], filename); - this.onUploadDesign([newFile]); + const fileList = [...files]; + fileList.forEach((file) => { + let filename = getFilename(file); + filename = validateImageName(file); + if (!filename || filename === 'image.png') { + filename = `design_${Date.now()}.png`; + } + const newFile = new File([file], filename); + this.onUploadDesign([newFile]); + }); } }, toggleOnPasteListener() { diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index f98f63529fc..f404fa4e0e8 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -43,7 +43,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { let pasteText; let addFileToForm; let updateAttachingMessage; - let isImage; let uploadFile; formTextarea.wrap('<div class="div-dropzone"></div>'); @@ -173,7 +172,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { return dropzoneInstance.addFile(file); }); }); - // eslint-disable-next-line consistent-return + handlePaste = (event) => { const pasteEvent = event.originalEvent; const { clipboardData } = pasteEvent; @@ -186,32 +185,22 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) { const text = converter.convertToTableMarkdown(); pasteText(text); } else { - const image = isImage(pasteEvent); - - if (image) { - event.preventDefault(); - const MAX_FILE_NAME_LENGTH = 246; - const filename = getFilename(pasteEvent) || 'image.png'; - const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH); - const text = `{{${truncateFilename}}}`; - pasteText(text); - - return uploadFile(image.getAsFile(), truncateFilename); - } - } - } - }; - - isImage = (data) => { - let i = 0; - while (i < data.clipboardData.items.length) { - const item = data.clipboardData.items[i]; - if (item.type.indexOf('image') !== -1) { - return item; + const fileList = [...clipboardData.files]; + fileList.forEach((file) => { + if (file.type.indexOf('image') !== -1) { + event.preventDefault(); + const MAX_FILE_NAME_LENGTH = 246; + + const filename = getFilename(file) || 'image.png'; + const truncateFilename = truncate(filename, MAX_FILE_NAME_LENGTH); + const text = `{{${truncateFilename}}}`; + pasteText(text); + + uploadFile(file, truncateFilename); + } + }); } - i += 1; } - return false; }; pasteText = (text, shouldPad) => { diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue index 0521e1eeea5..7cbfb35aeaa 100644 --- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue +++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue @@ -5,6 +5,7 @@ import { VALIDATE_INTEGRATION_FORM_EVENT, GET_JIRA_ISSUE_TYPES_EVENT, } from '~/integrations/constants'; +import { s__, __ } from '~/locale'; import eventHub from '../event_hub'; import JiraUpgradeCta from './jira_upgrade_cta.vue'; @@ -94,33 +95,38 @@ export default { eventHub.$emit(GET_JIRA_ISSUE_TYPES_EVENT); }, }, + i18n: { + sectionTitle: s__('JiraService|View Jira issues in GitLab'), + sectionDescription: s__( + 'JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of Jira issues and view any issue as read-only.', + ), + enableCheckboxLabel: s__('JiraService|Enable Jira issues'), + enableCheckboxHelp: s__( + 'JiraService|Warning: All GitLab users that have access to this GitLab project are able to view all issues from the Jira project specified below.', + ), + projectKeyLabel: s__('JiraService|Jira project key'), + projectKeyPlaceholder: s__('JiraService|For example, AB'), + requiredFieldFeedback: __('This field is required.'), + issueTrackerConflictWarning: s__( + 'JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used.', + ), + }, }; </script> <template> <div> - <gl-form-group - :label="s__('JiraService|View Jira issues in GitLab')" - label-for="jira-issue-settings" - > + <gl-form-group :label="$options.i18n.sectionTitle" label-for="jira-issue-settings"> <div id="jira-issue-settings"> <p> - {{ - s__( - 'JiraService|Work on Jira issues without leaving GitLab. Adds a Jira menu to access your list of Jira issues and view any issue as read-only.', - ) - }} + {{ $options.i18n.sectionDescription }} </p> <template v-if="showJiraIssuesIntegration"> <input name="service[issues_enabled]" type="hidden" :value="enableJiraIssues || false" /> <gl-form-checkbox v-model="enableJiraIssues" :disabled="isInheriting"> - {{ s__('JiraService|Enable Jira issues') }} + {{ $options.i18n.enableCheckboxLabel }} <template #help> - {{ - s__( - 'JiraService|Warning: All GitLab users that have access to this GitLab project are able to view all issues from the Jira project specified below.', - ) - }} + {{ $options.i18n.enableCheckboxHelp }} </template> </gl-form-checkbox> <template v-if="enableJiraIssues"> @@ -152,30 +158,25 @@ export default { </gl-form-group> <template v-if="showJiraIssuesIntegration"> <gl-form-group - :label="s__('JiraService|Jira project key')" + :label="$options.i18n.projectKeyLabel" label-for="service_project_key" - :invalid-feedback="__('This field is required.')" + :invalid-feedback="$options.i18n.requiredFieldFeedback" :state="validProjectKey" + data-testid="project-key-form-group" > <gl-form-input id="service_project_key" v-model="projectKey" name="service[project_key]" - :placeholder="s__('JiraService|For example, AB')" + :placeholder="$options.i18n.projectKeyPlaceholder" :required="enableJiraIssues" :state="validProjectKey" :disabled="!enableJiraIssues" :readonly="isInheriting" /> </gl-form-group> - <p v-if="gitlabIssuesEnabled"> - <gl-sprintf - :message=" - s__( - 'JiraService|Displaying Jira issues while leaving the GitLab issue functionality enabled might be confusing. Consider %{linkStart}disabling GitLab issues%{linkEnd} if they won’t otherwise be used.', - ) - " - > + <p v-if="gitlabIssuesEnabled" data-testid="conflict-warning-text"> + <gl-sprintf :message="$options.i18n.issueTrackerConflictWarning"> <template #link="{ content }"> <gl-link :href="editProjectPath" target="_blank">{{ content }}</gl-link> </template> diff --git a/app/assets/javascripts/lib/utils/file_upload.js b/app/assets/javascripts/lib/utils/file_upload.js index b8b63bf58d4..f99a4927338 100644 --- a/app/assets/javascripts/lib/utils/file_upload.js +++ b/app/assets/javascripts/lib/utils/file_upload.js @@ -15,13 +15,17 @@ export default (buttonSelector, fileSelector) => { }); }; -export const getFilename = ({ clipboardData }) => { - let value; - if (window.clipboardData && window.clipboardData.getData) { - value = window.clipboardData.getData('Text'); - } else if (clipboardData && clipboardData.getData) { - value = clipboardData.getData('text/plain'); +export const getFilename = (file) => { + let fileName; + if (file) { + fileName = file.name; } - value = value.split('\r'); - return value[0]; + + return fileName; +}; + +export const validateImageName = (file) => { + const fileName = file.name ? file.name : 'image.png'; + const legalImageRegex = /^[\w.\-+]+\.(png|jpg|jpeg|gif|bmp|tiff|ico|webp)$/; + return legalImageRegex.test(fileName) ? fileName : 'image.png'; }; diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index c2f5e275bb2..fb397e7c8c7 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -9,7 +9,7 @@ class Admin::HooksController < Admin::ApplicationController urgency :low, [:test] def index - @hooks = SystemHook.all + @hooks = SystemHook.all.load @hook = SystemHook.new end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 4242f918ea0..d4b1306cc5e 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -52,7 +52,7 @@ class InvitesController < ApplicationController end def current_user_matches_invite? - current_user.verified_emails.include?(@member.invite_email) + current_user.verified_email?(@member.invite_email) end def member? diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 35baf5d0ebb..4b4e6367a2a 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -16,7 +16,7 @@ class Projects::HooksController < Projects::ApplicationController urgency :low, [:test] def index - @hooks = @project.hooks + @hooks = @project.hooks.load @hook = ProjectHook.new end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 3033dac8246..4fe37352995 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -104,8 +104,7 @@ module Projects CreatePipelineWorker.perform_async(project.id, current_user.id, project.default_branch, :web, ignore_skip_ci: true, save_on_errors: false) # rubocop:enable CodeReuse/Worker - pipelines_link_start = '<a href="%{url}">'.html_safe % { url: project_pipelines_path(@project) } - flash[:toast] = _("A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details") % { pipelines_link_start: pipelines_link_start, pipelines_link_end: "</a>".html_safe } + flash[:toast] = _("A new Auto DevOps pipeline has been created, go to the Pipelines page for details") end def define_variables diff --git a/app/graphql/mutations/merge_requests/attention_required.rb b/app/graphql/mutations/merge_requests/attention_required.rb new file mode 100644 index 00000000000..51e1891aad3 --- /dev/null +++ b/app/graphql/mutations/merge_requests/attention_required.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Mutations + module MergeRequests + class AttentionRequired < Base + graphql_name 'MergeRequestAttentionRequired' + + argument :user_id, ::Types::GlobalIDType[::User], + loads: Types::UserType, + required: true, + description: <<~DESC + User ID for the user that has their attention requested. + DESC + + def resolve(project_path:, iid:, user:) + merge_request = authorized_find!(project_path: project_path, iid: iid) + + result = ::MergeRequests::AttentionRequiredService.new(project: merge_request.project, current_user: current_user, merge_request: merge_request, user: user).execute + + { + merge_request: merge_request, + errors: Array(result[:message]) + } + end + end + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index cd4c45d2942..bbcc3e819e0 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -69,6 +69,7 @@ module Types mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetAssignees mount_mutation Mutations::MergeRequests::ReviewerRereview + mount_mutation Mutations::MergeRequests::AttentionRequired, feature_flag: :mr_attention_requests mount_mutation Mutations::Metrics::Dashboard::Annotations::Create mount_mutation Mutations::Metrics::Dashboard::Annotations::Delete mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index e9dc271dbdd..b715993b6dc 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -24,6 +24,7 @@ module TodosHelper when Todo::UNMERGEABLE then 'Could not merge' when Todo::DIRECTLY_ADDRESSED then "directly addressed #{todo_action_subject(todo)} on" when Todo::MERGE_TRAIN_REMOVED then "Removed from Merge Train:" + when Todo::ATTENTION_REQUIRED then 'requested your attention on' end end diff --git a/app/models/loose_foreign_keys/modification_tracker.rb b/app/models/loose_foreign_keys/modification_tracker.rb index 12f3156b98c..6eb04608cd9 100644 --- a/app/models/loose_foreign_keys/modification_tracker.rb +++ b/app/models/loose_foreign_keys/modification_tracker.rb @@ -12,14 +12,24 @@ module LooseForeignKeys @delete_count_by_table = Hash.new { |h, k| h[k] = 0 } @update_count_by_table = Hash.new { |h, k| h[k] = 0 } @start_time = monotonic_time + @deletes_counter = Gitlab::Metrics.counter( + :loose_foreign_key_deletions, + 'The number of loose foreign key deletions' + ) + @updates_counter = Gitlab::Metrics.counter( + :loose_foreign_key_updates, + 'The number of loose foreign key updates' + ) end def add_deletions(table, count) @delete_count_by_table[table] += count + @deletes_counter.increment({ table: table }, count) end def add_updates(table, count) @update_count_by_table[table] += count + @updates_counter.increment({ table: table }, count) end def over_limit? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 9da6bb86b02..2aed45048a1 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1919,6 +1919,10 @@ class MergeRequest < ApplicationRecord true end + def find_assignee(user) + merge_request_assignees.find_by(user_id: user.id) + end + def find_reviewer(user) merge_request_reviewers.find_by(user_id: user.id) end diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb index 978e3af7fb4..f776ac4256c 100644 --- a/app/models/merge_request_diff_commit.rb +++ b/app/models/merge_request_diff_commit.rb @@ -114,18 +114,18 @@ class MergeRequestDiffCommit < ApplicationRecord end def author_name - commit_author.name + commit_author&.name end def author_email - commit_author.email + commit_author&.email end def committer_name - committer.name + committer&.name end def committer_email - committer.email + committer&.email end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index f93f21aba23..1dfc0168ede 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -140,6 +140,7 @@ class Namespace < ApplicationRecord 'COALESCE(SUM(ps.snippets_size), 0) AS snippets_size', 'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size', 'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size', + 'COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size', 'COALESCE(SUM(ps.packages_size), 0) AS packages_size', 'COALESCE(SUM(ps.uploads_size), 0) AS uploads_size' ) diff --git a/app/models/todo.rb b/app/models/todo.rb index 94a99603848..d0a6d1d9e23 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -18,6 +18,7 @@ class Todo < ApplicationRecord DIRECTLY_ADDRESSED = 7 MERGE_TRAIN_REMOVED = 8 # This is an EE-only feature REVIEW_REQUESTED = 9 + ATTENTION_REQUIRED = 10 ACTION_NAMES = { ASSIGNED => :assigned, @@ -28,7 +29,8 @@ class Todo < ApplicationRecord APPROVAL_REQUIRED => :approval_required, UNMERGEABLE => :unmergeable, DIRECTLY_ADDRESSED => :directly_addressed, - MERGE_TRAIN_REMOVED => :merge_train_removed + MERGE_TRAIN_REMOVED => :merge_train_removed, + ATTENTION_REQUIRED => :attention_required }.freeze belongs_to :author, class_name: "User" @@ -189,6 +191,10 @@ class Todo < ApplicationRecord action == REVIEW_REQUESTED end + def attention_required? + action == ATTENTION_REQUIRED + end + def merge_train_removed? action == MERGE_TRAIN_REMOVED end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 275ebcc7bcd..c7380768e32 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -6,13 +6,7 @@ # and existing service will use these one by one. # After all are migrated, we can remove this class. # -# New services should consider inheriting from: -# -# - BaseContainerService for services scoped by container (project or group) -# - BaseProjectService for services scoped to projects -# - BaseGroupService for services scoped to groups -# -# or, create a new base class and update this comment. +# For new services, please see https://docs.gitlab.com/ee/development/reusing_abstractions.html#service-classes class BaseService include BaseServiceUtility include Gitlab::Experiment::Dsl diff --git a/app/services/merge_requests/attention_required_service.rb b/app/services/merge_requests/attention_required_service.rb new file mode 100644 index 00000000000..54a549899fd --- /dev/null +++ b/app/services/merge_requests/attention_required_service.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module MergeRequests + class AttentionRequiredService < MergeRequests::BaseService + attr_accessor :merge_request, :user + + def initialize(project:, current_user:, merge_request:, user:) + super(project: project, current_user: current_user) + + @merge_request = merge_request + @user = user + end + + def execute + return error("Invalid permissions") unless can?(current_user, :update_merge_request, merge_request) + + if reviewer || assignee + reviewer&.update(state: :attention_required) + assignee&.update(state: :attention_required) + + notity_user + + success + else + error("User is not a reviewer or assignee of the merge request") + end + end + + private + + def notity_user + todo_service.create_attention_required_todo(merge_request, current_user, user) + end + + def assignee + merge_request.find_assignee(user) + end + + def reviewer + merge_request.find_reviewer(user) + end + end +end diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 71bb813f384..10779e38b75 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -217,6 +217,11 @@ class TodoService create_todos(reviewers, attributes) end + def create_attention_required_todo(target, author, users) + attributes = attributes_for_todo(target.project, target, author, Todo::ATTENTION_REQUIRED) + create_todos(users, attributes) + end + private def create_todos(users, attributes) diff --git a/app/views/shared/web_hooks/_index.html.haml b/app/views/shared/web_hooks/_index.html.haml index 794418b8336..f1eef5d7f0f 100644 --- a/app/views/shared/web_hooks/_index.html.haml +++ b/app/views/shared/web_hooks/_index.html.haml @@ -3,7 +3,7 @@ .card-header %h5 = hook_class.underscore.humanize.titleize.pluralize - (#{hooks.load.size}) + (#{hooks.size}) - if hooks.any? %ul.content-list diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 27a04e125ba..8baa97a7456 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -3228,6 +3228,29 @@ Input type: `MergeRequestAcceptInput` | <a id="mutationmergerequestaccepterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationmergerequestacceptmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. | +### `Mutation.mergeRequestAttentionRequired` + +Available only when feature flag `mr_attention_requests` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. + +Input type: `MergeRequestAttentionRequiredInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationmergerequestattentionrequiredclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationmergerequestattentionrequirediid"></a>`iid` | [`String!`](#string) | IID of the merge request to mutate. | +| <a id="mutationmergerequestattentionrequiredprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the merge request to mutate is in. | +| <a id="mutationmergerequestattentionrequireduserid"></a>`userId` | [`UserID!`](#userid) | User ID for the user that has their attention requested. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationmergerequestattentionrequiredclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationmergerequestattentionrequirederrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationmergerequestattentionrequiredmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. | + ### `Mutation.mergeRequestCreate` Input type: `MergeRequestCreateInput` diff --git a/doc/api/groups.md b/doc/api/groups.md index 7efecfc2c9c..5faa63585c1 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -100,13 +100,15 @@ GET /groups?statistics=true "parent_id": null, "created_at": "2020-01-15T12:36:29.590Z", "statistics": { - "storage_size" : 363, - "repository_size" : 33, - "wiki_size" : 100, - "lfs_objects_size" : 123, - "job_artifacts_size" : 57, + "storage_size": 363, + "repository_size": 33, + "wiki_size": 100, + "lfs_objects_size": 123, + "job_artifacts_size": 57, + "pipeline_artifacts_size": 0, "packages_size": 0, - "snippets_size" : 50 + "snippets_size": 50, + "uploads_size": 0 } } ] diff --git a/doc/api/projects.md b/doc/api/projects.md index 024362f3246..4ac6a2fe8dd 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -191,8 +191,10 @@ When the user is authenticated and `simple` is not set this returns something li "wiki_size" : 0, "lfs_objects_size": 0, "job_artifacts_size": 0, + "pipeline_artifacts_size": 0, "packages_size": 0, - "snippets_size": 0 + "snippets_size": 0, + "uploads_size": 0 }, "container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client", "_links": { @@ -303,8 +305,10 @@ When the user is authenticated and `simple` is not set this returns something li "wiki_size" : 0, "lfs_objects_size": 0, "job_artifacts_size": 0, + "pipeline_artifacts_size": 0, "packages_size": 0, - "snippets_size": 0 + "snippets_size": 0, + "uploads_size": 0 }, "container_registry_image_prefix": "registry.example.com/brightbox/puppet", "_links": { @@ -469,8 +473,10 @@ GET /users/:user_id/projects "wiki_size" : 0, "lfs_objects_size": 0, "job_artifacts_size": 0, + "pipeline_artifacts_size": 0, "packages_size": 0, - "snippets_size": 0 + "snippets_size": 0, + "uploads_size": 0 }, "container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client", "_links": { @@ -581,8 +587,10 @@ GET /users/:user_id/projects "wiki_size" : 0, "lfs_objects_size": 0, "job_artifacts_size": 0, + "pipeline_artifacts_size": 0, "packages_size": 0, - "snippets_size": 0 + "snippets_size": 0, + "uploads_size": 0 }, "container_registry_image_prefix": "registry.example.com/brightbox/puppet", "_links": { @@ -704,6 +712,10 @@ Example response: "repository_size": 1038090, "lfs_objects_size": 0, "job_artifacts_size": 0 + "pipeline_artifacts_size": 0, + "packages_size": 0, + "snippets_size": 0, + "uploads_size": 0 }, "container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client", "_links": { @@ -811,6 +823,10 @@ Example response: "repository_size": 2066080, "lfs_objects_size": 0, "job_artifacts_size": 0 + "pipeline_artifacts_size": 0, + "packages_size": 0, + "snippets_size": 0, + "uploads_size": 0 }, "container_registry_image_prefix": "registry.example.com/brightbox/puppet", "_links": { @@ -978,8 +994,10 @@ GET /projects/:id "wiki_size" : 0, "lfs_objects_size": 0, "job_artifacts_size": 0, + "pipeline_artifacts_size": 0, "packages_size": 0, - "snippets_size": 0 + "snippets_size": 0, + "uploads_size": 0 }, "container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client", "_links": { diff --git a/doc/ci/index.md b/doc/ci/index.md index 2f18bd28642..bad900cb181 100644 --- a/doc/ci/index.md +++ b/doc/ci/index.md @@ -129,13 +129,17 @@ See also: - [Enable or disable GitLab CI/CD in a project](enable_or_disable_ci.md). -## References +## Related topics Learn more about GitLab CI/CD: - [Why you might choose GitLab CI/CD](https://about.gitlab.com/blog/2016/10/17/gitlab-ci-oohlala/). - [Reasons you might migrate from another platform](https://about.gitlab.com/blog/2016/07/22/building-our-web-app-on-gitlab-ci/). - [5 Teams that made the switch to GitLab CI/CD](https://about.gitlab.com/blog/2019/04/25/5-teams-that-made-the-switch-to-gitlab-ci-cd/) +- If you use VS Code to edit your GitLab CI/CD configuration, the + [GitLab Workflow VS Code extension](../user/project/repository/vscode.md) helps you + [validate your configuration](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow#validate-gitlab-ci-configuration) + and [view your pipeline status](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow#information-about-your-branch-pipelines-mr-closing-issue). See also the [Why CI/CD?](https://docs.google.com/presentation/d/1OGgk2Tcxbpl7DJaIOzCX4Vqg3dlwfELC3u2jEeCBbDk) presentation. diff --git a/doc/ci/lint.md b/doc/ci/lint.md index 45152e5a0df..e58907ee0bd 100644 --- a/doc/ci/lint.md +++ b/doc/ci/lint.md @@ -14,6 +14,9 @@ issues as well. To access the CI Lint tool, navigate to **CI/CD > Pipelines** or **CI/CD > Jobs** in your project and click **CI lint**. +If you use VS Code, you can also validate your CI/CD configuration with the +[GitLab Workflow VS Code extension](../user/project/repository/vscode.md). + ## Validate basic logic and syntax By default, the CI lint checks the syntax of your CI YAML configuration and also runs diff --git a/doc/ci/pipelines/index.md b/doc/ci/pipelines/index.md index b2b2e26f30b..5507428c3f1 100644 --- a/doc/ci/pipelines/index.md +++ b/doc/ci/pipelines/index.md @@ -127,6 +127,11 @@ you can filter the pipeline list by: [Starting in GitLab 14.2](https://gitlab.com/gitlab-org/gitlab/-/issues/26621), you can change the pipeline column to display the pipeline ID or the pipeline IID. +If you use VS Code to edit your GitLab CI/CD configuration, the +[GitLab Workflow VS Code extension](../../user/project/repository/vscode.md) helps you +[validate your configuration](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow#validate-gitlab-ci-configuration) +and [view your pipeline status](https://marketplace.visualstudio.com/items?itemName=GitLab.gitlab-workflow#information-about-your-branch-pipelines-mr-closing-issue). + ### Run a pipeline manually Pipelines can be manually executed, with predefined or manually-specified [variables](../variables/index.md). diff --git a/doc/development/namespaces_storage_statistics.md b/doc/development/namespaces_storage_statistics.md index c6af0ddf9ea..e5263288210 100644 --- a/doc/development/namespaces_storage_statistics.md +++ b/doc/development/namespaces_storage_statistics.md @@ -53,7 +53,10 @@ SELECT split_part("rs".path, '/', 1) as root_path, COALESCE(SUM(ps.wiki_size), 0) AS wiki_size, COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size, COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size, - COALESCE(SUM(ps.packages_size), 0) AS packages_size + COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size, + COALESCE(SUM(ps.packages_size), 0) AS packages_size, + COALESCE(SUM(ps.snippets_size), 0) AS snippets_size, + COALESCE(SUM(ps.uploads_size), 0) AS uploads_size FROM "projects" INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project' INNER JOIN project_statistics ps ON ps.project_id = projects.id @@ -83,7 +86,10 @@ WITH refresh AS ( COALESCE(SUM(ps.wiki_size), 0) AS wiki_size, COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size, COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size, - COALESCE(SUM(ps.packages_size), 0) AS packages_size + COALESCE(SUM(ps.pipeline_artifacts_size), 0) AS pipeline_artifacts_size, + COALESCE(SUM(ps.packages_size), 0) AS packages_size, + COALESCE(SUM(ps.snippets_size), 0) AS snippets_size, + COALESCE(SUM(ps.uploads_size), 0) AS uploads_size FROM "projects" INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project' INNER JOIN project_statistics ps ON ps.project_id = projects.id @@ -94,7 +100,10 @@ SET storage_size = refresh.storage_size, wiki_size = refresh.wiki_size, lfs_objects_size = refresh.lfs_objects_size, build_artifacts_size = refresh.build_artifacts_size, - packages_size = refresh.packages_size + pipeline_artifacts_size = refresh.pipeline_artifacts_size, + packages_size = refresh.packages_size, + snippets_size = refresh.snippets_size, + uploads_size = refresh.uploads_size FROM refresh INNER JOIN routes rs ON rs.path = refresh.root_path AND rs.source_type = 'Namespace' WHERE namespace_storage_statistics.namespace_id = rs.source_id diff --git a/doc/development/reusing_abstractions.md b/doc/development/reusing_abstractions.md index 568e8a9d123..151cb076e41 100644 --- a/doc/development/reusing_abstractions.md +++ b/doc/development/reusing_abstractions.md @@ -133,8 +133,20 @@ Everything in `lib/api`. Everything that resides in `app/services`. +Services should consider inheriting from: + +- `BaseContainerService` for services scoped by container (project or group) +- `BaseProjectService` for services scoped to projects +- `BaseGroupService` for services scoped to groups + +or, create a new base class and update the list above. + +Legacy classes inherited from `BaseService` for historical reasons. + In Service classes the use of `execute` and `#execute` is preferred over `call` and `#call`. +Classes that are not service objects should be [created elsewhere](directory_structure.md#use-namespaces-to-define-bounded-contexts, such as in `lib`. + #### ServiceResponse Service classes usually have an `execute` method, which can return a diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 2acb79041b6..6a739d9e1a5 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -64,6 +64,9 @@ use the `GITLAB_TEST_EAGER_LOAD` environment variable: GITLAB_TEST_EAGER_LOAD=1 bin/rspec spec/models/project_spec.rb ``` +If your test depends on all the application code that is being loaded, add the `:eager_load` tag. +This ensures that the application code is eagerly loaded before the test execution. + ### Ruby warnings > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47767) in GitLab 13.7. diff --git a/doc/development/testing_guide/end_to_end/beginners_guide.md b/doc/development/testing_guide/end_to_end/beginners_guide.md index 7370cc5771b..27a87d25170 100644 --- a/doc/development/testing_guide/end_to_end/beginners_guide.md +++ b/doc/development/testing_guide/end_to_end/beginners_guide.md @@ -350,7 +350,7 @@ GITLAB_PASSWORD=<GDK root password> bundle exec bin/qa Test::Instance::All http: Where `<test_file>` is: - `qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb` when running the Login example. -- `qa/specs/features/browser_ui/2_plan/issues/create_issue_spec.rb` when running the Issue example. +- `qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb` when running the Issue example. ## End-to-end test merge request template diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index f6b847c1c9d..35b7d6f339b 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -47,16 +47,6 @@ Announced: 2021-08-22 ## 15.0 -### Legacy database configuration - -The syntax of [GitLabs database](https://docs.gitlab.com/omnibus/settings/database.html) -configuration located in `database.yml` is changing and the legacy format is deprecated. The legacy format -supported using a single PostgreSQL adapter, whereas the new format is changing to support multiple databases. The `main:` database needs to be defined as a first configuration item. - -This deprecation mainly impacts users compiling GitLab from source because Omnibus will handle this configuration automatically. - -Announced: 2021-09-22 - ### Audit events for repository push events Audit events for [repository events](https://docs.gitlab.com/ee/administration/audit_events.html#repository-push) are now deprecated and will be removed in GitLab 15.0. @@ -67,21 +57,31 @@ dramatically slow down GitLab instances. For this reason, they are being removed Announced: 2021-09-22 -### OmniAuth Kerberos gem +### GitLab Serverless -The `omniauth-kerberos` gem will be removed in our next major release, GitLab 15.0. +[GitLab Serverless](https://docs.gitlab.com/ee/user/project/clusters/serverless/) is a feature set to support Knative-based serverless development with automatic deployments and monitoring. -This gem has not been maintained and has very little usage. We therefore plan to remove support for this authentication method and recommend using the Kerberos [SPNEGO](https://en.wikipedia.org/wiki/SPNEGO) integration instead. You can follow the [upgrade instructions](https://docs.gitlab.com/ee/integration/kerberos.html#upgrading-from-password-based-to-ticket-based-kerberos-sign-ins) to upgrade from the `omniauth-kerberos` integration to the supported one. +We decided to remove the GitLab Serverless features as they never really resonated with our users. Besides, given the continuous development of Kubernetes and Knative, our current implementations do not even work with recent versions. -Note that we are not deprecating the Kerberos SPNEGO integration, only the old password-based Kerberos integration. +Announced: 2021-09-22 + +### Legacy database configuration + +The syntax of [GitLabs database](https://docs.gitlab.com/omnibus/settings/database.html) +configuration located in `database.yml` is changing and the legacy format is deprecated. The legacy format +supported using a single PostgreSQL adapter, whereas the new format is changing to support multiple databases. The `main:` database needs to be defined as a first configuration item. + +This deprecation mainly impacts users compiling GitLab from source because Omnibus will handle this configuration automatically. Announced: 2021-09-22 -### GitLab Serverless +### OmniAuth Kerberos gem -[GitLab Serverless](https://docs.gitlab.com/ee/user/project/clusters/serverless/) is a feature set to support Knative-based serverless development with automatic deployments and monitoring. +The `omniauth-kerberos` gem will be removed in our next major release, GitLab 15.0. -We decided to remove the GitLab Serverless features as they never really resonated with our users. Besides, given the continuous development of Kubernetes and Knative, our current implementations do not even work with recent versions. +This gem has not been maintained and has very little usage. We therefore plan to remove support for this authentication method and recommend using the Kerberos [SPNEGO](https://en.wikipedia.org/wiki/SPNEGO) integration instead. You can follow the [upgrade instructions](https://docs.gitlab.com/ee/integration/kerberos.html#upgrading-from-password-based-to-ticket-based-kerberos-sign-ins) to upgrade from the `omniauth-kerberos` integration to the supported one. + +Note that we are not deprecating the Kerberos SPNEGO integration, only the old password-based Kerberos integration. Announced: 2021-09-22 diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb index 048b7a3c15a..246fb819890 100644 --- a/lib/api/entities/group.rb +++ b/lib/api/entities/group.rb @@ -31,7 +31,10 @@ module API expose :wiki_size expose :lfs_objects_size expose :build_artifacts_size, as: :job_artifacts_size + expose :pipeline_artifacts_size + expose :packages_size expose :snippets_size + expose :uploads_size end end end diff --git a/lib/api/entities/project_statistics.rb b/lib/api/entities/project_statistics.rb index 70980e670b0..6544e8bc8ff 100644 --- a/lib/api/entities/project_statistics.rb +++ b/lib/api/entities/project_statistics.rb @@ -9,8 +9,10 @@ module API expose :wiki_size expose :lfs_objects_size expose :build_artifacts_size, as: :job_artifacts_size - expose :snippets_size + expose :pipeline_artifacts_size expose :packages_size + expose :snippets_size + expose :uploads_size end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 8090094d077..b4701f0d3a9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1471,7 +1471,7 @@ msgstr "" msgid "A merge request hasn't yet been merged" msgstr "" -msgid "A new Auto DevOps pipeline has been created, go to %{pipelines_link_start}Pipelines page%{pipelines_link_end} for details" +msgid "A new Auto DevOps pipeline has been created, go to the Pipelines page for details" msgstr "" msgid "A new Release %{tag} for %{name} was published. Visit the %{release_link_start}Releases page%{release_link_end} to read more about it." @@ -30430,6 +30430,9 @@ msgstr "" msgid "SecurityOrchestration|Scan execution" msgstr "" +msgid "SecurityOrchestration|Scan execution policies can only be created by project owners." +msgstr "" + msgid "SecurityOrchestration|Scan to be performed every %{cadence} on the %{branches}" msgstr "" diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index b8da451cbb0..7fb2723dd01 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -210,7 +210,7 @@ RSpec.describe 'Database schema' do # We are skipping GEO models for now as it adds up complexity describe 'for jsonb columns' do - it 'uses json schema validator' do + it 'uses json schema validator', :eager_load do columns_name_with_jsonb.each do |hash| next if models_by_table_name[hash["table_name"]].nil? diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index 87fb8955dcc..f9ab780d2d6 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -103,6 +103,20 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do expect(page).to have_content('You are already a member of this group.') end end + + context 'when email case doesnt match', :js do + let(:invite_email) { 'User@example.com' } + let(:user) { create(:user, email: 'user@example.com') } + + before do + sign_in(user) + visit invite_path(group_invite.raw_invite_token) + end + + it 'accepts invite' do + expect(page).to have_content('You have been granted Developer access to group Owned.') + end + end end context 'when declining the invitation from invitation reminder email' do diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js index ce79feae2e7..427161a391b 100644 --- a/spec/frontend/design_management/pages/index_spec.js +++ b/spec/frontend/design_management/pages/index_spec.js @@ -669,6 +669,20 @@ describe('Design management index page', () => { expect(variables.files).toEqual(event.clipboardData.files.map((f) => new File([f], ''))); }); + it('display original file name', () => { + event.clipboardData.files = [new File([new Blob()], 'test.png', { type: 'image/png' })]; + document.dispatchEvent(event); + + const [{ mutation, variables }] = mockMutate.mock.calls[0]; + expect(mutation).toBe(uploadDesignMutation); + expect(variables).toStrictEqual({ + files: expect.any(Array), + iid: '1', + projectPath: 'project-path', + }); + expect(variables.files[0].name).toEqual('test.png'); + }); + it('renames a design if it has an image.png filename', () => { event.clipboardData.getData = () => 'image.png'; document.dispatchEvent(event); diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js index acf7d0780cd..12e10f7c5f4 100644 --- a/spec/frontend/dropzone_input_spec.js +++ b/spec/frontend/dropzone_input_spec.js @@ -71,6 +71,7 @@ describe('dropzone_input', () => { triggerPasteEvent({ types: ['text/plain', 'text/html', 'text/rtf', 'Files'], getData: () => longFileName, + files: [new File([new Blob()], longFileName, { type: 'image/png' })], items: [ { kind: 'file', @@ -84,6 +85,24 @@ describe('dropzone_input', () => { await waitForPromises(); expect(axiosMock.history.post[0].data.get('file').name).toHaveLength(246); }); + + it('display original file name in comment box', async () => { + const axiosMock = new MockAdapter(axios); + triggerPasteEvent({ + types: ['Files'], + files: [new File([new Blob()], 'test.png', { type: 'image/png' })], + items: [ + { + kind: 'file', + type: 'image/png', + getAsFile: () => new Blob(), + }, + ], + }); + axiosMock.onPost().reply(httpStatusCodes.OK, { link: { markdown: 'foo' } }); + await waitForPromises(); + expect(axiosMock.history.post[0].data.get('file').name).toEqual('test.png'); + }); }); describe('shows error message', () => { diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js index 119afbfecfe..3a664b652ac 100644 --- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js +++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js @@ -1,7 +1,10 @@ import { GlFormCheckbox, GlFormInput } from '@gitlab/ui'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { GET_JIRA_ISSUE_TYPES_EVENT } from '~/integrations/constants'; +import { + GET_JIRA_ISSUE_TYPES_EVENT, + VALIDATE_INTEGRATION_FORM_EVENT, +} from '~/integrations/constants'; import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue'; import eventHub from '~/integrations/edit/event_hub'; import { createStore } from '~/integrations/edit/store'; @@ -17,12 +20,17 @@ describe('JiraIssuesFields', () => { upgradePlanPath: 'https://gitlab.com', }; - const createComponent = ({ isInheriting = false, props, ...options } = {}) => { + const createComponent = ({ + isInheriting = false, + mountFn = mountExtended, + props, + ...options + } = {}) => { store = createStore({ defaultState: isInheriting ? {} : undefined, }); - wrapper = mountExtended(JiraIssuesFields, { + wrapper = mountFn(JiraIssuesFields, { propsData: { ...defaultProps, ...props }, store, stubs: ['jira-issue-creation-vulnerabilities'], @@ -38,12 +46,19 @@ describe('JiraIssuesFields', () => { const findEnableCheckboxDisabled = () => findEnableCheckbox().find('[type=checkbox]').attributes('disabled'); const findProjectKey = () => wrapper.findComponent(GlFormInput); + const findProjectKeyFormGroup = () => wrapper.findByTestId('project-key-form-group'); const findPremiumUpgradeCTA = () => wrapper.findByTestId('premium-upgrade-cta'); const findUltimateUpgradeCTA = () => wrapper.findByTestId('ultimate-upgrade-cta'); const findJiraForVulnerabilities = () => wrapper.findByTestId('jira-for-vulnerabilities'); + const findConflictWarning = () => wrapper.findByTestId('conflict-warning-text'); const setEnableCheckbox = async (isEnabled = true) => findEnableCheckbox().vm.$emit('input', isEnabled); + const assertProjectKeyState = (expectedStateValue) => { + expect(findProjectKey().attributes('state')).toBe(expectedStateValue); + expect(findProjectKeyFormGroup().attributes('state')).toBe(expectedStateValue); + }; + describe('template', () => { describe.each` showJiraIssuesIntegration | showJiraVulnerabilitiesIntegration @@ -151,19 +166,18 @@ describe('JiraIssuesFields', () => { }); describe('GitLab issues warning', () => { - const expectedText = 'Consider disabling GitLab issues'; - - it('contains warning when GitLab issues is enabled', () => { - createComponent(); - - expect(wrapper.text()).toContain(expectedText); - }); - - it('does not contain warning when GitLab issues is disabled', () => { - createComponent({ props: { gitlabIssuesEnabled: false } }); - - expect(wrapper.text()).not.toContain(expectedText); - }); + it.each` + gitlabIssuesEnabled | scenario + ${true} | ${'displays conflict warning'} + ${false} | ${'does not display conflict warning'} + `( + '$scenario when `gitlabIssuesEnabled` is `$gitlabIssuesEnabled`', + ({ gitlabIssuesEnabled }) => { + createComponent({ props: { gitlabIssuesEnabled } }); + + expect(findConflictWarning().exists()).toBe(gitlabIssuesEnabled); + }, + ); }); describe('Vulnerabilities creation', () => { @@ -211,5 +225,44 @@ describe('JiraIssuesFields', () => { expect(eventHubEmitSpy).toHaveBeenCalledWith(GET_JIRA_ISSUE_TYPES_EVENT); }); }); + + describe('Project key input field', () => { + beforeEach(() => { + createComponent({ + props: { + initialProjectKey: '', + initialEnableJiraIssues: true, + }, + mountFn: shallowMountExtended, + }); + }); + + it('sets Project Key `state` attribute to `true` by default', () => { + assertProjectKeyState('true'); + }); + + describe('when event hub recieves `VALIDATE_INTEGRATION_FORM_EVENT` event', () => { + describe('with no project key', () => { + it('sets Project Key `state` attribute to `undefined`', async () => { + eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT); + await wrapper.vm.$nextTick(); + + assertProjectKeyState(undefined); + }); + }); + + describe('when project key is set', () => { + it('sets Project Key `state` attribute to `true`', async () => { + eventHub.$emit(VALIDATE_INTEGRATION_FORM_EVENT); + + // set the project key + await findProjectKey().vm.$emit('input', 'AB'); + await wrapper.vm.$nextTick(); + + assertProjectKeyState('true'); + }); + }); + }); + }); }); }); diff --git a/spec/frontend/lib/utils/file_upload_spec.js b/spec/frontend/lib/utils/file_upload_spec.js index 1dff5d4f925..ff11107ea60 100644 --- a/spec/frontend/lib/utils/file_upload_spec.js +++ b/spec/frontend/lib/utils/file_upload_spec.js @@ -1,4 +1,4 @@ -import fileUpload, { getFilename } from '~/lib/utils/file_upload'; +import fileUpload, { getFilename, validateImageName } from '~/lib/utils/file_upload'; describe('File upload', () => { beforeEach(() => { @@ -64,13 +64,23 @@ describe('File upload', () => { }); describe('getFilename', () => { - it('returns first value correctly', () => { - const event = { - clipboardData: { - getData: () => 'test.png\rtest.txt', - }, - }; - - expect(getFilename(event)).toBe('test.png'); + it('returns file name', () => { + const file = new File([], 'test.jpg'); + + expect(getFilename(file)).toBe('test.jpg'); + }); +}); + +describe('file name validator', () => { + it('validate file name', () => { + const file = new File([], 'test.jpg'); + + expect(validateImageName(file)).toBe('test.jpg'); + }); + + it('illegal file name should be rename to image.png', () => { + const file = new File([], 'test<.png'); + + expect(validateImageName(file)).toBe('image.png'); }); }); diff --git a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb index 4903f624469..ea13326cbee 100644 --- a/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/plain_markdown_pipeline_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Banzai::Pipeline::PlainMarkdownPipeline do expect(result[:escaped_literals]).to be_truthy end - it 'ensure we handle all the GitLab reference characters' do + it 'ensure we handle all the GitLab reference characters', :eager_load do reference_chars = ObjectSpace.each_object(Class).map do |klass| next unless klass.included_modules.include?(Referable) next unless klass.respond_to?(:reference_prefix) diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index 7e031bdd263..4f3b95e43cd 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -375,7 +375,7 @@ RSpec.describe ReactiveCaching, :use_clean_rails_memory_store_caching do end describe 'classes including this concern' do - it 'sets reactive_cache_work_type' do + it 'sets reactive_cache_work_type', :eager_load do classes = ObjectSpace.each_object(Class).select do |klass| klass < described_class && klass.name end diff --git a/spec/models/loose_foreign_keys/modification_tracker_spec.rb b/spec/models/loose_foreign_keys/modification_tracker_spec.rb index 13e5f920d05..069ccf85141 100644 --- a/spec/models/loose_foreign_keys/modification_tracker_spec.rb +++ b/spec/models/loose_foreign_keys/modification_tracker_spec.rb @@ -50,6 +50,26 @@ RSpec.describe LooseForeignKeys::ModificationTracker do end end + describe '#add_deletions' do + it 'increments a Prometheus counter' do + counter = Gitlab::Metrics.registry.get(:loose_foreign_key_deletions) + + subject.add_deletions(:users, 4) + + expect(counter.get(table: :users)).to eq(4) + end + end + + describe '#add_updates' do + it 'increments a Prometheus counter' do + counter = Gitlab::Metrics.registry.get(:loose_foreign_key_updates) + + subject.add_updates(:users, 4) + + expect(counter.get(table: :users)).to eq(4) + end + end + describe '#stats' do it 'exposes stats' do freeze_time do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 75e46bb7873..ab20e6cb020 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -568,26 +568,30 @@ RSpec.describe Namespace do create(:project, namespace: namespace, statistics: build(:project_statistics, - namespace: namespace, - repository_size: 101, - wiki_size: 505, - lfs_objects_size: 202, - build_artifacts_size: 303, - packages_size: 404, - snippets_size: 605)) + namespace: namespace, + repository_size: 101, + wiki_size: 505, + lfs_objects_size: 202, + build_artifacts_size: 303, + pipeline_artifacts_size: 707, + packages_size: 404, + snippets_size: 605, + uploads_size: 808)) end let(:project2) do create(:project, namespace: namespace, statistics: build(:project_statistics, - namespace: namespace, - repository_size: 10, - wiki_size: 50, - lfs_objects_size: 20, - build_artifacts_size: 30, - packages_size: 40, - snippets_size: 60)) + namespace: namespace, + repository_size: 10, + wiki_size: 50, + lfs_objects_size: 20, + build_artifacts_size: 30, + pipeline_artifacts_size: 70, + packages_size: 40, + snippets_size: 60, + uploads_size: 80)) end it "sums all project storage counters in the namespace" do @@ -595,13 +599,15 @@ RSpec.describe Namespace do project2 statistics = described_class.with_statistics.find(namespace.id) - expect(statistics.storage_size).to eq 2330 + expect(statistics.storage_size).to eq 3995 expect(statistics.repository_size).to eq 111 expect(statistics.wiki_size).to eq 555 expect(statistics.lfs_objects_size).to eq 222 expect(statistics.build_artifacts_size).to eq 333 + expect(statistics.pipeline_artifacts_size).to eq 777 expect(statistics.packages_size).to eq 444 expect(statistics.snippets_size).to eq 665 + expect(statistics.uploads_size).to eq 888 end it "correctly handles namespaces without projects" do @@ -612,8 +618,10 @@ RSpec.describe Namespace do expect(statistics.wiki_size).to eq 0 expect(statistics.lfs_objects_size).to eq 0 expect(statistics.build_artifacts_size).to eq 0 + expect(statistics.pipeline_artifacts_size).to eq 0 expect(statistics.packages_size).to eq 0 expect(statistics.snippets_size).to eq 0 + expect(statistics.uploads_size).to eq 0 end end diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb index ead6238b2f4..5fbf1a9c502 100644 --- a/spec/models/project_statistics_spec.rb +++ b/spec/models/project_statistics_spec.rb @@ -325,12 +325,14 @@ RSpec.describe ProjectStatistics do lfs_objects_size: 3, snippets_size: 2, pipeline_artifacts_size: 3, + build_artifacts_size: 3, + packages_size: 6, uploads_size: 5 ) statistics.reload - expect(statistics.storage_size).to eq 19 + expect(statistics.storage_size).to eq 28 end it 'works during wiki_size backfill' do diff --git a/spec/requests/api/graphql/mutations/merge_requests/attention_required_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/attention_required_spec.rb new file mode 100644 index 00000000000..328167af16d --- /dev/null +++ b/spec/requests/api/graphql/mutations/merge_requests/attention_required_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Setting attention required for reviewer' do + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:merge_request) { create(:merge_request, reviewers: [user]) } + let(:project) { merge_request.project } + let(:user) { create(:user) } + let(:input) { { user_id: global_id_of(user) } } + + let(:mutation) do + variables = { + project_path: project.full_path, + iid: merge_request.iid.to_s + } + graphql_mutation(:merge_request_attention_required, variables.merge(input), + <<-QL.strip_heredoc + clientMutationId + errors + QL + ) + end + + def mutation_response + graphql_mutation_response(:merge_request_attention_required) + end + + def mutation_errors + mutation_response['errors'] + end + + before do + project.add_developer(current_user) + project.add_developer(user) + end + + it 'returns an error if the user is not allowed to update the merge request' do + post_graphql_mutation(mutation, current_user: create(:user)) + + expect(graphql_errors).not_to be_empty + end + + describe 'reviewer does not exist' do + let(:input) { { user_id: global_id_of(create(:user)) } } + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_errors).not_to be_empty + end + end + + describe 'reviewer exists' do + it 'does not return an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_errors).to be_empty + end + end +end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 2c7e2ecff85..fc596bff87e 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -319,12 +319,15 @@ RSpec.describe API::Groups do it "includes statistics if requested" do attributes = { - storage_size: 2392, + storage_size: 4093, repository_size: 123, wiki_size: 456, lfs_objects_size: 234, build_artifacts_size: 345, - snippets_size: 1234 + pipeline_artifacts_size: 456, + packages_size: 567, + snippets_size: 1234, + uploads_size: 678 }.stringify_keys exposed_attributes = attributes.dup exposed_attributes['job_artifacts_size'] = exposed_attributes.delete('build_artifacts_size') diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index fe7f4ebe407..da7ccef851e 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -359,7 +359,7 @@ RSpec.describe API::Projects do statistics = json_response.find { |p| p['id'] == project.id }['statistics'] expect(statistics).to be_present - expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size', 'packages_size') + expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'pipeline_artifacts_size', 'snippets_size', 'packages_size', 'uploads_size') end it "does not include license by default" do diff --git a/spec/services/merge_requests/attention_required_service_spec.rb b/spec/services/merge_requests/attention_required_service_spec.rb new file mode 100644 index 00000000000..2996b719c05 --- /dev/null +++ b/spec/services/merge_requests/attention_required_service_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::AttentionRequiredService do + let(:current_user) { create(:user) } + let(:user) { create(:user) } + let(:assignee_user) { create(:user) } + let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [assignee_user]) } + let(:reviewer) { merge_request.find_reviewer(user) } + let(:assignee) { merge_request.find_assignee(assignee_user) } + let(:project) { merge_request.project } + let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) } + let(:result) { service.execute } + let(:todo_service) { spy('todo service') } + + before do + allow(service).to receive(:todo_service).and_return(todo_service) + + project.add_developer(current_user) + project.add_developer(user) + end + + describe '#execute' do + context 'invalid permissions' do + let(:service) { described_class.new(project: project, current_user: create(:user), merge_request: merge_request, user: user) } + + it 'returns an error' do + expect(result[:status]).to eq :error + end + end + + context 'reviewer does not exist' do + let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: create(:user)) } + + it 'returns an error' do + expect(result[:status]).to eq :error + end + end + + context 'reviewer exists' do + it 'returns success' do + expect(result[:status]).to eq :success + end + + it 'updates reviewers state' do + service.execute + reviewer.reload + + expect(reviewer.state).to eq 'attention_required' + end + + it 'creates a new todo for the reviewer' do + expect(todo_service).to receive(:create_attention_required_todo).with(merge_request, current_user, user) + + service.execute + end + end + + context 'assignee exists' do + let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: assignee_user) } + + it 'returns success' do + expect(result[:status]).to eq :success + end + + it 'updates assignees state' do + service.execute + assignee.reload + + expect(assignee.state).to eq 'attention_required' + end + + it 'creates a new todo for the reviewer' do + expect(todo_service).to receive(:create_attention_required_todo).with(merge_request, current_user, assignee_user) + + service.execute + end + end + + context 'assignee is the same as reviewer' do + let(:merge_request) { create(:merge_request, reviewers: [user], assignees: [user]) } + let(:service) { described_class.new(project: project, current_user: current_user, merge_request: merge_request, user: user) } + let(:assignee) { merge_request.find_assignee(user) } + + it 'updates reviewers and assignees state' do + service.execute + reviewer.reload + assignee.reload + + expect(reviewer.state).to eq 'attention_required' + expect(assignee.state).to eq 'attention_required' + end + end + end +end diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb index 170d99f4642..71ad23bc68c 100644 --- a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb +++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb @@ -8,7 +8,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do let_it_be(:merge_request) { create(:merge_request) } describe '#CHECKS' do - it 'contains every subclass of the base checks service' do + it 'contains every subclass of the base checks service', :eager_load do expect(described_class::CHECKS).to contain_exactly(*MergeRequests::Mergeability::CheckBaseService.subclasses) end end @@ -19,7 +19,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do let(:params) { {} } let(:success_result) { Gitlab::MergeRequests::Mergeability::CheckResult.success } - context 'when every check is skipped' do + context 'when every check is skipped', :eager_load do before do MergeRequests::Mergeability::CheckBaseService.subclasses.each do |subclass| expect_next_instance_of(subclass) do |service| diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 6a8e6dc8970..a175e482f91 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -1218,6 +1218,17 @@ RSpec.describe TodoService do end end + describe '#create_attention_required_todo' do + let(:target) { create(:merge_request, author: author, source_project: project) } + let(:user) { create(:user) } + + it 'creates a todo for user' do + service.create_attention_required_todo(target, author, user) + + should_create_todo(user: user, target: target, action: Todo::ATTENTION_REQUIRED) + end + end + def should_create_todo(attributes = {}) attributes.reverse_merge!( project: project, diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index c8664598691..5de63acf66d 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -431,6 +431,10 @@ RSpec.configure do |config| Gitlab::Metrics.reset_registry! end + config.before(:example, :eager_load) do + Rails.application.eager_load! + end + # This makes sure the `ApplicationController#can?` method is stubbed with the # original implementation for all view specs. config.before(:each, type: :view) do diff --git a/tooling/deprecations/docs.rb b/tooling/deprecations/docs.rb index 67ff7a932b4..0f649024b60 100644 --- a/tooling/deprecations/docs.rb +++ b/tooling/deprecations/docs.rb @@ -20,9 +20,11 @@ module Deprecations YAML.load_file(file) end - deprecations = VersionSorter.sort(deprecations) { |d| d["removal_milestone"] } + deps = VersionSorter.sort(deprecations) { |d| d["removal_milestone"] } - milestones = deprecations.map { |d| d["removal_milestone"] }.uniq + deprecations = deps.sort_by { |d| d["name"] } + + milestones = deps.map { |d| d["removal_milestone"] }.uniq template = Rails.root.join("data/deprecations/templates/_deprecation_template.md.erb") diff --git a/workhorse/Makefile b/workhorse/Makefile index 0e8c47ae35c..3cf592b0cff 100644 --- a/workhorse/Makefile +++ b/workhorse/Makefile @@ -13,6 +13,8 @@ else BUILD_TIME := $(shell date -u "$(DATE_FMT)") endif GOBUILD := go build -ldflags "-X main.Version=$(VERSION_STRING) -X main.BuildTime=$(BUILD_TIME)" +GITALY := tmp/tests/gitaly/_build/bin/gitaly +GITALY_PID_FILE := gitaly.pid EXE_ALL := gitlab-resize-image gitlab-zip-cat gitlab-zip-metadata gitlab-workhorse INSTALL := install BUILD_TAGS := tracer_static tracer_static_jaeger continuous_profiler_stackdriver @@ -63,7 +65,16 @@ install: $(EXE_ALL) .PHONY: test test: prepare-tests $(call message,$@) - @go test -tags "$(BUILD_TAGS)" ./... + go test -tags "$(BUILD_TAGS)" ./... ;\ + status="$$?" ;\ + if [ -f "$(GITALY_PID_FILE)" ] ; then \ + echo "Clean up Gitaly server for workhorse integration test" ;\ + kill -9 $$(cat $(GITALY_PID_FILE)) ;\ + rm $(GITALY_PID_FILE) ;\ + else \ + echo "Gitaly integration test not running" ;\ + fi ;\ + exit "$$status" @echo SUCCESS .PHONY: clean @@ -82,9 +93,27 @@ clean-build: rm -rf $(TARGET_DIR) .PHONY: prepare-tests +prepare-tests: run-gitaly prepare-tests: testdata/data/group/test.git $(EXE_ALL) prepare-tests: testdata/scratch +.PHONY: run-gitaly +run-gitaly: gitaly.pid + +$(GITALY_PID_FILE): gitaly.toml + @{ \ + if [ -z "$${GITALY_ADDRESS+x}" ] ; then \ + echo "To run gitaly integration tests set GITALY_ADDRESS=tcp://127.0.0.1:8075" ; \ + else \ + cd .. ; \ + GITALY_TESTING_NO_GIT_HOOKS=1 GITALY_PID_FILE=workhorse/$(GITALY_PID_FILE) $(GITALY) workhorse/gitaly.toml ; \ + fi \ + } & + +gitaly.toml: ../tmp/tests/gitaly/config.toml + sed -e 's/^socket_path.*$$/listen_addr = "0.0.0.0:8075"/;s/^\[auth\]$$//;s/^token.*$$//;s/^internal_socket_dir.*$$//' \ + $< > $@ + testdata/data/group/test.git: $(call message,$@) git clone --quiet --bare https://gitlab.com/gitlab-org/gitlab-test.git $@ diff --git a/workhorse/gitaly_integration_test.go b/workhorse/gitaly_integration_test.go index 48de45d7935..95e0a03ab6b 100644 --- a/workhorse/gitaly_integration_test.go +++ b/workhorse/gitaly_integration_test.go @@ -16,6 +16,7 @@ import ( "strings" "testing" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitaly/v14/proto/go/gitalypb" @@ -53,6 +54,10 @@ func realGitalyOkBody(t *testing.T) *api.Response { return realGitalyAuthResponse(gitOkBody(t)) } +func realGitalyOkBodyWithSidechannel(t *testing.T) *api.Response { + return realGitalyAuthResponse(gitOkBodyWithSidechannel(t)) +} + func ensureGitalyRepository(t *testing.T, apiResponse *api.Response) error { ctx, namespace, err := gitaly.NewNamespaceClient(context.Background(), apiResponse.GitalyServer) if err != nil { @@ -83,10 +88,18 @@ func ensureGitalyRepository(t *testing.T, apiResponse *api.Response) error { } func TestAllowedClone(t *testing.T) { + testAllowedClone(t, realGitalyOkBody(t)) +} + +func TestAllowedCloneWithSidechannel(t *testing.T) { + gitaly.InitializeSidechannelRegistry(logrus.StandardLogger()) + testAllowedClone(t, realGitalyOkBodyWithSidechannel(t)) +} + +func testAllowedClone(t *testing.T, apiResponse *api.Response) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server - apiResponse := realGitalyOkBody(t) require.NoError(t, ensureGitalyRepository(t, apiResponse)) // Prepare test server and backend @@ -107,10 +120,18 @@ func TestAllowedClone(t *testing.T) { } func TestAllowedShallowClone(t *testing.T) { + testAllowedShallowClone(t, realGitalyOkBody(t)) +} + +func TestAllowedShallowCloneWithSidechannel(t *testing.T) { + gitaly.InitializeSidechannelRegistry(logrus.StandardLogger()) + testAllowedShallowClone(t, realGitalyOkBodyWithSidechannel(t)) +} + +func testAllowedShallowClone(t *testing.T, apiResponse *api.Response) { skipUnlessRealGitaly(t) // Create the repository in the Gitaly server - apiResponse := realGitalyOkBody(t) require.NoError(t, ensureGitalyRepository(t, apiResponse)) // Prepare test server and backend |