diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-09 15:08:59 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-09 15:08:59 +0000 |
commit | 4a14cfd1959c6a03758d0a75afe7b4277cf113ec (patch) | |
tree | caa9aa524ee10076f94a6369227aaf566cbb6e74 | |
parent | faeb202bd4a4099d4cff5a5717915883ac51422f (diff) | |
download | gitlab-ce-4a14cfd1959c6a03758d0a75afe7b4277cf113ec.tar.gz |
Add latest changes from gitlab-org/gitlab@master
100 files changed, 1261 insertions, 335 deletions
diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index 4d25ee9e4bd..05e2b6dca7e 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -303,7 +303,40 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo }); } +/* eslint-disable @gitlab/require-i18n-strings */ +export function keypressNoteText(e) { + if (this.selectionStart === this.selectionEnd) { + return; + } + const keys = { + '*': '**{text}**', // wraps with bold character + _: '_{text}_', // wraps with italic character + '`': '`{text}`', // wraps with inline character + "'": "'{text}'", // single quotes + '"': '"{text}"', // double quotes + '[': '[{text}]', // brackets + '{': '{{text}}', // braces + '(': '({text})', // parentheses + '<': '<{text}>', // angle brackets + }; + const tag = keys[e.key]; + + if (tag) { + updateText({ + tag, + textArea: this, + blockTag: '', + wrap: true, + select: '', + tagContent: '', + }); + e.preventDefault(); + } +} +/* eslint-enable @gitlab/require-i18n-strings */ + export function addMarkdownListeners(form) { + $('.markdown-area').on('keydown', keypressNoteText); return $('.js-md', form) .off('click') .on('click', function() { @@ -340,5 +373,6 @@ export function addEditorMarkdownListeners(editor) { } export function removeMarkdownListeners(form) { + $('.markdown-area').off('keydown'); return $('.js-md', form).off('click'); } diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js index b0cdad627a6..69d219d29f7 100644 --- a/app/assets/javascripts/pages/admin/groups/show/index.js +++ b/app/assets/javascripts/pages/admin/groups/show/index.js @@ -1,3 +1,23 @@ -import UsersSelect from '../../../../users_select'; +import Vue from 'vue'; +import UsersSelect from '~/users_select'; +import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; -document.addEventListener('DOMContentLoaded', () => new UsersSelect()); +function mountRemoveMemberModal() { + const el = document.querySelector('.js-remove-member-modal'); + if (!el) { + return false; + } + + return new Vue({ + el, + render(createComponent) { + return createComponent(RemoveMemberModal); + }, + }); +} + +document.addEventListener('DOMContentLoaded', () => { + mountRemoveMemberModal(); + + new UsersSelect(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index d6b1e747aec..d86c5e2ddb8 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -1,7 +1,25 @@ -import ProjectsList from '../../../projects_list'; -import NamespaceSelect from '../../../namespace_select'; +import Vue from 'vue'; +import ProjectsList from '~/projects_list'; +import NamespaceSelect from '~/namespace_select'; +import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; + +function mountRemoveMemberModal() { + const el = document.querySelector('.js-remove-member-modal'); + if (!el) { + return false; + } + + return new Vue({ + el, + render(createComponent) { + return createComponent(RemoveMemberModal); + }, + }); +} document.addEventListener('DOMContentLoaded', () => { + mountRemoveMemberModal(); + new ProjectsList(); // eslint-disable-line no-new document diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js new file mode 100644 index 00000000000..e146592e134 --- /dev/null +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import Members from 'ee_else_ce/members'; +import memberExpirationDate from '~/member_expiration_date'; +import UsersSelect from '~/users_select'; +import groupsSelect from '~/groups_select'; +import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; + +function mountRemoveMemberModal() { + const el = document.querySelector('.js-remove-member-modal'); + if (!el) { + return false; + } + + return new Vue({ + el, + render(createComponent) { + return createComponent(RemoveMemberModal); + }, + }); +} + +document.addEventListener('DOMContentLoaded', () => { + groupsSelect(); + memberExpirationDate(); + memberExpirationDate('.js-access-expiration-date-groups'); + mountRemoveMemberModal(); + + new Members(); // eslint-disable-line no-new + new UsersSelect(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js deleted file mode 100644 index 0c732922e81..00000000000 --- a/app/assets/javascripts/pages/groups/group_members/index/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable no-new */ - -import Members from 'ee_else_ce/members'; -import memberExpirationDate from '~/member_expiration_date'; -import UsersSelect from '~/users_select'; -import groupsSelect from '~/groups_select'; - -document.addEventListener('DOMContentLoaded', () => { - memberExpirationDate(); - memberExpirationDate('.js-access-expiration-date-groups'); - new Members(); - groupsSelect(); - new UsersSelect(); -}); diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index f39765818e7..e146592e134 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -1,12 +1,30 @@ +import Vue from 'vue'; import Members from 'ee_else_ce/members'; -import memberExpirationDate from '../../../member_expiration_date'; -import UsersSelect from '../../../users_select'; -import groupsSelect from '../../../groups_select'; +import memberExpirationDate from '~/member_expiration_date'; +import UsersSelect from '~/users_select'; +import groupsSelect from '~/groups_select'; +import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; + +function mountRemoveMemberModal() { + const el = document.querySelector('.js-remove-member-modal'); + if (!el) { + return false; + } + + return new Vue({ + el, + render(createComponent) { + return createComponent(RemoveMemberModal); + }, + }); +} document.addEventListener('DOMContentLoaded', () => { - memberExpirationDate('.js-access-expiration-date-groups'); groupsSelect(); memberExpirationDate(); + memberExpirationDate('.js-access-expiration-date-groups'); + mountRemoveMemberModal(); + new Members(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/projects/releases/new/index.js b/app/assets/javascripts/pages/projects/releases/new/index.js new file mode 100644 index 00000000000..0e314aacf8a --- /dev/null +++ b/app/assets/javascripts/pages/projects/releases/new/index.js @@ -0,0 +1,7 @@ +import ZenMode from '~/zen_mode'; +import initNewRelease from '~/releases/mount_new'; + +document.addEventListener('DOMContentLoaded', () => { + new ZenMode(); // eslint-disable-line no-new + initNewRelease(); +}); diff --git a/app/assets/javascripts/releases/components/app_new.vue b/app/assets/javascripts/releases/components/app_new.vue new file mode 100644 index 00000000000..563f76b3281 --- /dev/null +++ b/app/assets/javascripts/releases/components/app_new.vue @@ -0,0 +1,9 @@ +<script> +export default { + name: 'ReleaseNewApp', + components: {}, +}; +</script> +<template> + <div></div> +</template> diff --git a/app/assets/javascripts/releases/mount_new.js b/app/assets/javascripts/releases/mount_new.js new file mode 100644 index 00000000000..eb02c194c59 --- /dev/null +++ b/app/assets/javascripts/releases/mount_new.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import ReleaseNewApp from './components/app_new.vue'; +import createStore from './stores'; +import createDetailModule from './stores/modules/detail'; + +export default () => { + const el = document.getElementById('js-new-release-page'); + + const store = createStore({ + modules: { + detail: createDetailModule(el.dataset), + }, + }); + + return new Vue({ + el, + store, + render: h => h(ReleaseNewApp), + }); +}; diff --git a/app/assets/javascripts/releases/stores/modules/detail/state.js b/app/assets/javascripts/releases/stores/modules/detail/state.js index 6d0d102c719..966c1c00ef5 100644 --- a/app/assets/javascripts/releases/stores/modules/detail/state.js +++ b/app/assets/javascripts/releases/stores/modules/detail/state.js @@ -1,17 +1,17 @@ export default ({ projectId, - tagName, - releasesPagePath, markdownDocsPath, markdownPreviewPath, updateReleaseApiDocsPath, releaseAssetsDocsPath, manageMilestonesPath, newMilestonePath, + + tagName = null, + releasesPagePath = null, + defaultBranch = null, }) => ({ projectId, - tagName, - releasesPagePath, markdownDocsPath, markdownPreviewPath, updateReleaseApiDocsPath, @@ -19,6 +19,10 @@ export default ({ manageMilestonesPath, newMilestonePath, + tagName, + releasesPagePath, + defaultBranch, + /** The Release object */ release: null, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index d147c32b58b..897f706290d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -1,4 +1,5 @@ <script> +import Mousetrap from 'mousetrap'; import { escape } from 'lodash'; import { n__, s__, sprintf } from '~/locale'; import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility'; @@ -74,6 +75,17 @@ export default { : ''; }, }, + mounted() { + Mousetrap.bind('b', this.copyBranchName); + }, + beforeDestroy() { + Mousetrap.unbind('b'); + }, + methods: { + copyBranchName() { + this.$refs.copyBranchNameButton.$el.click(); + }, + }, }; </script> <template> @@ -89,6 +101,7 @@ export default { class="label-branch label-truncate js-source-branch" v-html="mr.sourceBranchLink" /><clipboard-button + ref="copyBranchNameButton" :text="branchNameClipboardData" :title="__('Copy branch name')" css-class="btn-default btn-transparent btn-clipboard" diff --git a/app/assets/javascripts/vue_shared/components/remove_member_modal.vue b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue new file mode 100644 index 00000000000..08b5fff780c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/remove_member_modal.vue @@ -0,0 +1,77 @@ +<script> +import { GlFormCheckbox, GlModal } from '@gitlab/ui'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import csrf from '~/lib/utils/csrf'; +import { __ } from '~/locale'; + +export default { + actionCancel: { + text: __('Cancel'), + }, + csrf, + components: { + GlFormCheckbox, + GlModal, + }, + data() { + return { + modalData: {}, + }; + }, + computed: { + isAccessRequest() { + return parseBoolean(this.modalData.isAccessRequest); + }, + actionText() { + return this.isAccessRequest ? __('Deny access request') : __('Remove member'); + }, + actionPrimary() { + return { + text: this.actionText, + attributes: { + variant: 'danger', + }, + }; + }, + }, + mounted() { + document.addEventListener('click', this.handleClick); + }, + beforeDestroy() { + document.removeEventListener('click', this.handleClick); + }, + methods: { + handleClick(event) { + const removeButton = event.target.closest('.js-remove-member-button'); + if (removeButton) { + this.modalData = removeButton.dataset; + this.$refs.modal.show(); + } + }, + submitForm() { + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <gl-modal + ref="modal" + modal-id="remove-member-modal" + :action-cancel="$options.actionCancel" + :action-primary="actionPrimary" + :title="actionText" + @primary="submitForm" + > + <form ref="form" :action="modalData.memberPath" method="post"> + <p data-testid="modal-message">{{ modalData.message }}</p> + + <input ref="method" type="hidden" name="_method" value="delete" /> + <input :value="$options.csrf.token" type="hidden" name="authenticity_token" /> + <gl-form-checkbox v-if="!isAccessRequest" name="unassign_issuables"> + {{ __('Also unassign this user from related issues and merge requests') }} + </gl-form-checkbox> + </form> + </gl-modal> +</template> diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index eb9684c7b3c..fd11d0e3a69 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -45,6 +45,7 @@ display: block; float: left; margin-right: 10px; + max-width: 250px; } .new-file-name, @@ -139,10 +140,6 @@ clear: both; } } - - .editor-ref { - max-width: 250px; - } } } diff --git a/app/controllers/concerns/renders_member_access.rb b/app/controllers/concerns/renders_member_access.rb index 955ac1a1bc8..745830181c1 100644 --- a/app/controllers/concerns/renders_member_access.rb +++ b/app/controllers/concerns/renders_member_access.rb @@ -7,12 +7,6 @@ module RendersMemberAccess groups end - def prepare_projects_for_rendering(projects) - preload_max_member_access_for_collection(Project, projects) - - projects - end - private # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/controllers/concerns/renders_projects_list.rb b/app/controllers/concerns/renders_projects_list.rb new file mode 100644 index 00000000000..be45c676ad6 --- /dev/null +++ b/app/controllers/concerns/renders_projects_list.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module RendersProjectsList + def prepare_projects_for_rendering(projects) + preload_max_member_access_for_collection(Project, projects) + + # Call the forks count method on every project, so the BatchLoader would load them all at + # once when the entities are rendered + projects.each(&:forks_count) + + projects + end +end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 4028ea46406..ad64b6c4f94 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -3,6 +3,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController include ParamsBackwardCompatibility include RendersMemberAccess + include RendersProjectsList include SortingHelper include SortingPreference include FiltersEvents diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 705a586d614..f1f41e67a4c 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -4,6 +4,7 @@ class Explore::ProjectsController < Explore::ApplicationController include PageLimiter include ParamsBackwardCompatibility include RendersMemberAccess + include RendersProjectsList include SortingHelper include SortingPreference diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index ebc81976529..b93f6384e0c 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -3,6 +3,7 @@ class Projects::ForksController < Projects::ApplicationController include ContinueParams include RendersMemberAccess + include RendersProjectsList include Gitlab::Utils::StrongMemoize # Authorize diff --git a/app/controllers/projects/pipelines/tests_controller.rb b/app/controllers/projects/pipelines/tests_controller.rb index 6e4b5155a4f..f03274bf32e 100644 --- a/app/controllers/projects/pipelines/tests_controller.rb +++ b/app/controllers/projects/pipelines/tests_controller.rb @@ -2,35 +2,58 @@ module Projects module Pipelines - class TestsController < Projects::ApplicationController - before_action :pipeline - before_action :authorize_read_pipeline! - before_action :authorize_read_build! + class TestsController < Projects::Pipelines::ApplicationController before_action :validate_feature_flag! + before_action :authorize_read_build! + before_action :builds, only: [:show] def summary respond_to do |format| format.json do - render json: TestReportSerializer + render json: TestReportSummarySerializer .new(project: project, current_user: @current_user) .represent(pipeline.test_report_summary) end end end + def show + respond_to do |format| + format.json do + render json: TestSuiteSerializer + .new(project: project, current_user: @current_user) + .represent(test_suite, details: true) + end + end + end + private def validate_feature_flag! render_404 unless Feature.enabled?(:build_report_summary, project) end - def pipeline - project.all_pipelines.find(tests_params[:id]) + # rubocop: disable CodeReuse/ActiveRecord + def builds + pipeline.latest_builds.where(id: build_params) + end + + def build_params + return [] unless params[:build_ids] + + params[:build_ids].split(",") end - def tests_params - params.permit(:id) + def test_suite + if builds.present? + builds.map do |build| + build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new) + end.sum + else + render_404 + end end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index ea3ef23fe4c..3d48fb9c803 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -26,11 +26,11 @@ class Projects::ReleasesController < Projects::ApplicationController def show return render_404 unless Feature.enabled?(:release_show_page, project, default_enabled: true) + end - respond_to do |format| - format.html do - render :show - end + def new + unless Feature.enabled?(:new_release_page, project) + return redirect_to(new_project_tag_path(@project)) end end @@ -38,22 +38,12 @@ class Projects::ReleasesController < Projects::ApplicationController redirect_to link.url end - protected + private def releases ReleasesFinder.new(@project, current_user).execute end - def edit - respond_to do |format| - format.html do - render :edit - end - end - end - - private - def authorize_update_release! access_denied! unless can?(current_user, :update_release, release) end diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 24452f9a188..14469877e14 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -13,10 +13,15 @@ class RootController < Dashboard::ProjectsController before_action :redirect_unlogged_user, if: -> { current_user.nil? } before_action :redirect_logged_user, if: -> { current_user.present? } + # We only need to load the projects when the user is logged in but did not + # configure a dashboard. In which case we render projects. We can do that straight + # from the #index action. + skip_before_action :projects def index # n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/40260 Gitlab::GitalyClient.allow_n_plus_1_calls do + projects super end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 3cc07585c3f..95ea31fa977 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,6 +3,7 @@ class UsersController < ApplicationController include RoutableActions include RendersMemberAccess + include RendersProjectsList include ControllerWithCrossProjectAccessCheck include Gitlab::NoteableMetadata diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 73219ca9e1e..1487e496107 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -12,6 +12,8 @@ module Types present_using IssuePresenter + field :id, GraphQL::ID_TYPE, null: false, + description: "ID of the issue" field :iid, GraphQL::ID_TYPE, null: false, description: "Internal ID of the issue" field :title, GraphQL::STRING_TYPE, null: false, diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index c9a0521228c..d66f67fbb60 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -48,11 +48,11 @@ module MembersHelper "#{request.path}?#{options.to_param}" end - def member_path(member, unassign_issuables: false) + def member_path(member) if member.is_a?(GroupMember) - group_group_member_path(member.source, member, { unassign_issuables: unassign_issuables }) + group_group_member_path(member.source, member) else - project_project_member_path(member.source, member, { unassign_issuables: unassign_issuables }) + project_project_member_path(member.source, member) end end diff --git a/app/helpers/releases_helper.rb b/app/helpers/releases_helper.rb index 1238567a4ed..a3d944c64cc 100644 --- a/app/helpers/releases_helper.rb +++ b/app/helpers/releases_helper.rb @@ -18,21 +18,40 @@ module ReleasesHelper illustration_path: illustration, documentation_path: help_page }.tap do |data| - data[:new_release_path] = new_project_tag_path(@project) if can?(current_user, :create_release, @project) + if can?(current_user, :create_release, @project) + data[:new_release_path] = if Feature.enabled?(:new_release_page, @project) + new_project_release_path(@project) + else + new_project_tag_path(@project) + end + end end end def data_for_edit_release_page + new_edit_pages_shared_data.merge( + tag_name: @release.tag, + releases_page_path: project_releases_path(@project, anchor: @release.tag) + ) + end + + def data_for_new_release_page + new_edit_pages_shared_data.merge( + default_branch: @project.default_branch + ) + end + + private + + def new_edit_pages_shared_data { project_id: @project.id, - tag_name: @release.tag, markdown_preview_path: preview_markdown_path(@project), markdown_docs_path: help_page_path('user/markdown'), - releases_page_path: project_releases_path(@project, anchor: @release.tag), update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'), release_assets_docs_path: help_page(anchor: 'release-assets'), manage_milestones_path: project_milestones_path(@project), - new_milestone_path: new_project_milestone_url(@project) + new_milestone_path: new_project_milestone_path(@project) } end end diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 8bad9303046..d2aa336d12f 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -34,6 +34,7 @@ module Ci license_management: 'gl-license-management-report.json', license_scanning: 'gl-license-scanning-report.json', performance: 'performance.json', + browser_performance: 'browser-performance.json', metrics: 'metrics.txt', lsif: 'lsif.json', dotenv: '.env', @@ -73,6 +74,7 @@ module Ci license_management: :raw, license_scanning: :raw, performance: :raw, + browser_performance: :raw, terraform: :raw, requirements: :raw, coverage_fuzzing: :raw @@ -93,6 +95,7 @@ module Ci lsif metrics performance + browser_performance sast secret_detection requirements @@ -180,7 +183,7 @@ module Ci codequality: 9, ## EE-specific license_management: 10, ## EE-specific license_scanning: 101, ## EE-specific till 13.0 - performance: 11, ## EE-specific + performance: 11, ## EE-specific till 13.2 metrics: 12, ## EE-specific metrics_referee: 13, ## runner referees network_referee: 14, ## runner referees @@ -192,7 +195,8 @@ module Ci cluster_applications: 20, secret_detection: 21, ## EE-specific requirements: 22, ## EE-specific - coverage_fuzzing: 23 ## EE-specific + coverage_fuzzing: 23, ## EE-specific + browser_performance: 24 ## EE-specific } enum file_format: { diff --git a/app/models/product_analytics_event.rb b/app/models/product_analytics_event.rb new file mode 100644 index 00000000000..552b6585db7 --- /dev/null +++ b/app/models/product_analytics_event.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ProductAnalyticsEvent < ApplicationRecord + self.table_name = 'product_analytics_events_experimental' + + # Ignore that the partition key :project_id is part of the formal primary key + self.primary_key = :id + + belongs_to :project + + # There is no default Rails timestamps in the table. + # collector_tstamp is a timestamp when a collector recorded an event. + scope :order_by_time, -> { order(collector_tstamp: :desc) } + + # If we decide to change this scope to use date_trunc('day', collector_tstamp), + # we should remember that a btree index on collector_tstamp will be no longer effective. + scope :timerange, ->(duration, today = Time.zone.today) { + where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1) + } +end diff --git a/app/models/project.rb b/app/models/project.rb index 909695774fb..f60b5490757 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2153,7 +2153,13 @@ class Project < ApplicationRecord # rubocop: disable CodeReuse/ServiceClass def forks_count - Projects::ForksCountService.new(self).count + BatchLoader.for(self).batch do |projects, loader| + fork_count_per_project = ::Projects::BatchForksCountService.new(projects).refresh_cache_and_retrieve_data + + fork_count_per_project.each do |project, count| + loader.call(project, count) + end + end end # rubocop: enable CodeReuse/ServiceClass diff --git a/app/serializers/test_report_summary_entity.rb b/app/serializers/test_report_summary_entity.rb new file mode 100644 index 00000000000..5995ca007d6 --- /dev/null +++ b/app/serializers/test_report_summary_entity.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class TestReportSummaryEntity < TestReportEntity + expose :test_suites, using: TestSuiteSummaryEntity do |summary| + summary.test_suites.values + end +end diff --git a/app/serializers/test_report_summary_serializer.rb b/app/serializers/test_report_summary_serializer.rb new file mode 100644 index 00000000000..6077a4e87bb --- /dev/null +++ b/app/serializers/test_report_summary_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class TestReportSummarySerializer < BaseSerializer + entity TestReportSummaryEntity +end diff --git a/app/serializers/test_suite_serializer.rb b/app/serializers/test_suite_serializer.rb new file mode 100644 index 00000000000..f11d0fbe7e6 --- /dev/null +++ b/app/serializers/test_suite_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class TestSuiteSerializer < BaseSerializer + entity TestSuiteEntity +end diff --git a/app/serializers/test_suite_summary_entity.rb b/app/serializers/test_suite_summary_entity.rb new file mode 100644 index 00000000000..6718b31a7f5 --- /dev/null +++ b/app/serializers/test_suite_summary_entity.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class TestSuiteSummaryEntity < TestSuiteEntity + expose :build_ids do |summary| + summary.build_ids + end +end diff --git a/app/services/jira/jql_builder_service.rb b/app/services/jira/jql_builder_service.rb index 2a4b18fcc8c..ddf7c4e30a6 100644 --- a/app/services/jira/jql_builder_service.rb +++ b/app/services/jira/jql_builder_service.rb @@ -12,6 +12,9 @@ module Jira @jira_project_key = jira_project_key @search = params[:search] @labels = params[:labels] + @status = params[:status] + @reporter = params[:author_username] + @assignee = params[:assignee_username] @sort = params[:sort] || DEFAULT_SORT @sort_direction = params[:sort_direction] || DEFAULT_SORT_DIRECTION end @@ -25,12 +28,15 @@ module Jira private - attr_reader :jira_project_key, :sort, :sort_direction, :search, :labels + attr_reader :jira_project_key, :sort, :sort_direction, :search, :labels, :status, :reporter, :assignee def jql_filters [ by_project, by_labels, + by_status, + by_reporter, + by_assignee, by_summary_and_description ].compact.join(' AND ') end @@ -52,10 +58,28 @@ module Jira labels.map { |label| %Q[labels = "#{escape_quotes(label)}"] }.join(' AND ') end + def by_status + return if status.blank? + + %Q[status = "#{escape_quotes(status)}"] + end + def order_by "order by #{sort} #{sort_direction}" end + def by_reporter + return if reporter.blank? + + %Q[reporter = "#{escape_quotes(reporter)}"] + end + + def by_assignee + return if assignee.blank? + + %Q[assignee = "#{escape_quotes(assignee)}"] + end + def escape_quotes(param) param.gsub('\\', '\\\\\\').gsub('"', '\\"') end diff --git a/app/services/projects/batch_forks_count_service.rb b/app/services/projects/batch_forks_count_service.rb index 6467744a435..d12772b40ff 100644 --- a/app/services/projects/batch_forks_count_service.rb +++ b/app/services/projects/batch_forks_count_service.rb @@ -5,6 +5,21 @@ # because the service use maps to retrieve the project ids module Projects class BatchForksCountService < Projects::BatchCountService + def refresh_cache_and_retrieve_data + count_services = @projects.map { |project| count_service.new(project) } + + values = Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do + Rails.cache.fetch_multi(*(count_services.map { |ser| ser.cache_key } )) { |key| nil } + end + + results_per_service = Hash[count_services.zip(values.values)] + projects_to_refresh = results_per_service.select { |_k, value| value.nil? } + projects_to_refresh = recreate_cache(projects_to_refresh) + + results_per_service.update(projects_to_refresh) + results_per_service.transform_keys { |k| k.project } + end + # rubocop: disable CodeReuse/ActiveRecord def global_count @global_count ||= begin @@ -18,5 +33,13 @@ module Projects def count_service ::Projects::ForksCountService end + + def recreate_cache(projects_to_refresh) + projects_to_refresh.each_with_object({}) do |(service, _v), hash| + count = global_count[service.project.id].to_i + service.refresh_cache { count } + hash[service] = count + end + end end end diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb index ca85e2dc281..848d8d54104 100644 --- a/app/services/projects/forks_count_service.rb +++ b/app/services/projects/forks_count_service.rb @@ -3,6 +3,8 @@ module Projects # Service class for getting and caching the number of forks of a project. class ForksCountService < Projects::CountService + attr_reader :project + def cache_key_name 'forks_count' end diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index e105091e773..4b0e0b9c697 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -1,6 +1,8 @@ - add_to_breadcrumbs _("Groups"), admin_groups_path - breadcrumb_title @group.name - page_title @group.name, _("Groups") + +.js-remove-member-modal %h3.page-title = _('Group: %{group_name}') % { group_name: @group.full_name } diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 1edfedb00f8..5a1bf7b0f74 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -3,6 +3,7 @@ - page_title @project.full_name, _("Projects") - @content_class = "admin-projects" +.js-remove-member-modal %h3.page-title Project: #{@project.full_name} = link_to edit_project_path(@project), class: "btn btn-nr float-right" do diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index c488e75da09..b9ea8316bbc 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -4,6 +4,7 @@ - pending_active = params[:search_invited].present? - total_count = @members.count + @group.shared_with_group_links.count +.js-remove-member-modal .project-members-page.gl-mt-3 %h4 = _("Group members") diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 3e9a77d01b4..80df8581a9b 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -309,6 +309,10 @@ %td.shortcut %kbd p %td= _('Previous unresolved discussion') + %tr + %td.shortcut + %kbd b + %td= _('Copy source branch name') %tbody %tr %th diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index d8158376009..1e9cf68f3a5 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -4,7 +4,7 @@ .file-holder-bottom-radius.file-holder.file.gl-mb-3 .js-file-title.file-title.align-items-center.clearfix{ data: { current_action: action } } - .editor-ref.block-truncated + .editor-ref.block-truncated.has-tooltip{ title: ref } = sprite_icon('fork', size: 12) = ref - if current_action?(:edit) || current_action?(:update) diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 88e53a99ced..ba964e5cd37 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,6 +1,7 @@ - page_title _("Members") - can_admin_project_members = can?(current_user, :admin_project_member, @project) +.js-remove-member-modal .row.gl-mt-3 .col-lg-12 - if project_can_be_shared? diff --git a/app/views/projects/releases/new.html.haml b/app/views/projects/releases/new.html.haml index 5391a8047dc..4348035a324 100644 --- a/app/views/projects/releases/new.html.haml +++ b/app/views/projects/releases/new.html.haml @@ -1 +1,3 @@ - page_title s_('Releases|New Release') + +#js-new-release-page{ data: data_for_new_release_page } diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 6b8739194d4..dbb8a1198df 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -118,11 +118,9 @@ data: { confirm: leave_confirmation_message(member.source) }, class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}" - elsif !user&.project_bot? - = link_to member_path(member.member), - method: :delete, - data: { confirm: remove_member_message(member), qa_selector: 'delete_member_button' }, - class: "btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}", - title: remove_member_title(member) do + %button{ data: { member_path: member_path(member.member), message: remove_member_message(member), is_access_request: member.request?.to_s, qa_selector: 'delete_member_button' }, + class: "js-remove-member-button btn btn-remove align-self-center m-0 #{'ml-sm-2' unless force_mobile_view}", + title: remove_member_title(member) } %span{ class: ('d-block d-sm-none' unless force_mobile_view) } = _("Delete") - unless force_mobile_view diff --git a/changelogs/unreleased/218040-cablett-graphql-issue-id.yml b/changelogs/unreleased/218040-cablett-graphql-issue-id.yml new file mode 100644 index 00000000000..8d697d554d8 --- /dev/null +++ b/changelogs/unreleased/218040-cablett-graphql-issue-id.yml @@ -0,0 +1,5 @@ +--- +title: Expose issue ID via GraphQL +merge_request: 36412 +author: +type: changed diff --git a/changelogs/unreleased/220316-redis-n-1-in-api-v4-groups-id-projects-forks-count-key.yml b/changelogs/unreleased/220316-redis-n-1-in-api-v4-groups-id-projects-forks-count-key.yml new file mode 100644 index 00000000000..49ebf8739ab --- /dev/null +++ b/changelogs/unreleased/220316-redis-n-1-in-api-v4-groups-id-projects-forks-count-key.yml @@ -0,0 +1,5 @@ +--- +title: Use BatchLoader for Project.forks_count to limit calls to Redis +merge_request: 35328 +author: +type: performance diff --git a/changelogs/unreleased/36720-frontend-provide-option-to-unassign-removed-user-from-issuables.yml b/changelogs/unreleased/36720-frontend-provide-option-to-unassign-removed-user-from-issuables.yml new file mode 100644 index 00000000000..af4358e573a --- /dev/null +++ b/changelogs/unreleased/36720-frontend-provide-option-to-unassign-removed-user-from-issuables.yml @@ -0,0 +1,5 @@ +--- +title: Add option to unassign member from issuables when removing them from a project +merge_request: 34946 +author: +type: added diff --git a/changelogs/unreleased/bw-surround-text-wth-char.yml b/changelogs/unreleased/bw-surround-text-wth-char.yml new file mode 100644 index 00000000000..7395bb9c26b --- /dev/null +++ b/changelogs/unreleased/bw-surround-text-wth-char.yml @@ -0,0 +1,5 @@ +--- +title: Surround selected text in markdown fields on certain key presses +merge_request: 25748 +author: +type: added diff --git a/changelogs/unreleased/nfriend-add-copy-branch-name-shortcut.yml b/changelogs/unreleased/nfriend-add-copy-branch-name-shortcut.yml new file mode 100644 index 00000000000..b039955edc9 --- /dev/null +++ b/changelogs/unreleased/nfriend-add-copy-branch-name-shortcut.yml @@ -0,0 +1,5 @@ +--- +title: Add keyboard shortcut ('b') to copy MR source branch name on MR page +merge_request: 36338 +author: +type: added diff --git a/changelogs/unreleased/ps-fix-single-file-editor-long-branch.yml b/changelogs/unreleased/ps-fix-single-file-editor-long-branch.yml new file mode 100644 index 00000000000..0618487fd74 --- /dev/null +++ b/changelogs/unreleased/ps-fix-single-file-editor-long-branch.yml @@ -0,0 +1,5 @@ +--- +title: Fix single file editor with long branch name +merge_request: 36371 +author: +type: fixed diff --git a/config/routes/pipelines.rb b/config/routes/pipelines.rb index c100526180e..50269d6e6ba 100644 --- a/config/routes/pipelines.rb +++ b/config/routes/pipelines.rb @@ -26,11 +26,11 @@ resources :pipelines, only: [:index, :new, :create, :show, :destroy] do resources :stages, only: [], param: :name do post :play_manual end + end - resources :tests, only: [], controller: 'pipelines/tests' do - collection do - get :summary - end + resources :tests, only: [:show], param: :suite_name, controller: 'pipelines/tests' do + collection do + get :summary end end end diff --git a/db/fixtures/development/27_product_analytics_events.rb b/db/fixtures/development/27_product_analytics_events.rb new file mode 100644 index 00000000000..19237afd8ea --- /dev/null +++ b/db/fixtures/development/27_product_analytics_events.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +Gitlab::Seeder.quiet do + # The data set takes approximately 2 minutes to load, + # so its put behind the flag. To seed this data use the flag and the filter: + # SEED_PRODUCT_ANALYTICS_EVENTS=1 FILTER=product_analytics_events rake db:seed_fu + flag = 'SEED_PRODUCT_ANALYTICS_EVENTS' + + if ENV[flag] + Project.all.sample(2).each do |project| + # Let's generate approx a week of events from now into the past with 1 minute step. + # To add some differentiation we add a random offset of up to 45 seconds. + 10000.times do |i| + dvce_created_tstamp = DateTime.now - i.minute - rand(45).seconds + + # Add a random delay to collector timestamp. Up to 2 seconds. + collector_tstamp = dvce_created_tstamp + rand(3).second + + ProductAnalyticsEvent.create!( + project_id: project.id, + platform: ["web", "mob", "mob", "app"].sample, + collector_tstamp: collector_tstamp, + dvce_created_tstamp: dvce_created_tstamp, + event: nil, + event_id: SecureRandom.uuid, + name_tracker: "sp", + v_tracker: "js-2.14.0", + v_collector: Gitlab::VERSION, + v_etl: Gitlab::VERSION, + domain_userid: SecureRandom.uuid, + domain_sessionidx: 4, + page_url: "#{project.web_url}/-/product_analytics/test", + page_title: 'Test page', + page_referrer: "#{project.web_url}/-/product_analytics/test", + br_lang: ["en-US", "en-US", "en-GB", "nl", "fi"].sample, # https://www.andiamo.co.uk/resources/iso-language-codes/ + br_features_pdf: true, + br_cookies: [true, true, true, false].sample, + br_colordepth: ["24", "24", "16", "8"].sample, + os_timezone: ["America/Los_Angeles", "America/Los_Angeles", "America/Lima", "Asia/Dubai", "Africa/Bangui"].sample, # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + doc_charset: ["UTF-8", "UTF-8", "UTF-8", "DOS", "EUC"].sample, + domain_sessionid: SecureRandom.uuid + ) + end + + unless Feature.enabled?(:product_analytics, project) + if Feature.enable(:product_analytics, project) + puts "Product analytics feature was enabled for #{project.full_path}" + end + end + + puts "10K events added to #{project.full_path}" + end + else + puts "Skipped. Use the `#{flag}` environment variable to enable." + end +end diff --git a/db/migrate/20200707094341_add_browser_performance_to_plan_limits.rb b/db/migrate/20200707094341_add_browser_performance_to_plan_limits.rb new file mode 100644 index 00000000000..ef0bea88ead --- /dev/null +++ b/db/migrate/20200707094341_add_browser_performance_to_plan_limits.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddBrowserPerformanceToPlanLimits < ActiveRecord::Migration[6.0] + DOWNTIME = false + + def change + add_column :plan_limits, "ci_max_artifact_size_browser_performance", :integer, default: 0, null: false + end +end diff --git a/db/structure.sql b/db/structure.sql index 65453a0c3f9..1804ff157c0 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13814,7 +13814,8 @@ CREATE TABLE public.plan_limits ( ci_max_artifact_size_cluster_applications integer DEFAULT 0 NOT NULL, ci_max_artifact_size_secret_detection integer DEFAULT 0 NOT NULL, ci_max_artifact_size_requirements integer DEFAULT 0 NOT NULL, - ci_max_artifact_size_coverage_fuzzing integer DEFAULT 0 NOT NULL + ci_max_artifact_size_coverage_fuzzing integer DEFAULT 0 NOT NULL, + ci_max_artifact_size_browser_performance integer DEFAULT 0 NOT NULL ); CREATE SEQUENCE public.plan_limits_id_seq @@ -23643,5 +23644,6 @@ COPY "schema_migrations" (version) FROM STDIN; 20200706005325 20200706170536 20200707071941 +20200707094341 \. diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index ccb89123c54..c77e5ecc185 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -5748,6 +5748,11 @@ type Issue implements Noteable { healthStatus: HealthStatus """ + ID of the issue + """ + id: ID! + + """ Internal ID of the issue """ iid: ID! diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 7fef43de22d..3f96d1235cd 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -15822,6 +15822,24 @@ "deprecationReason": null }, { + "name": "id", + "description": "ID of the issue", + "args": [ + + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "iid", "description": "Internal ID of the issue", "args": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index fba74f5dc13..c8b8136c294 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -860,6 +860,7 @@ Represents a Group Member | `dueDate` | Time | Due date of the issue | | `epic` | Epic | Epic to which this issue belongs | | `healthStatus` | HealthStatus | Current health status. Returns null if `save_issuable_health_status` feature flag is disabled. | +| `id` | ID! | ID of the issue | | `iid` | ID! | Internal ID of the issue | | `iteration` | Iteration | Iteration of the issue | | `milestone` | Milestone | Milestone of the issue | diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md index b56c3ce7ded..c2aad1ac6e4 100644 --- a/doc/ci/pipelines/job_artifacts.md +++ b/doc/ci/pipelines/job_artifacts.md @@ -251,10 +251,10 @@ dashboards. > - Introduced in GitLab 11.5. > - Requires GitLab Runner 11.5 and above. -The `performance` report collects [Performance metrics](../../user/project/merge_requests/browser_performance_testing.md) +The `performance` report collects [Browser Performance Testing metrics](../../user/project/merge_requests/browser_performance_testing.md) as artifacts. -The collected Performance report will be uploaded to GitLab as an artifact and will +The collected Browser Performance report will be uploaded to GitLab as an artifact and will be automatically shown in merge requests. #### `artifacts:reports:metrics` **(PREMIUM)** diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 7015bb9fc31..62dadaed9f5 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -117,7 +117,7 @@ The following table lists available parameters for jobs: | [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. | | [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. | | [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. | -| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0),`artifacts:reports:performance` and `artifacts:reports:metrics`. | +| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0), `artifacts:reports:performance` and `artifacts:reports:metrics`. | | [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. | | [`coverage`](#coverage) | Code coverage settings for a given job. | | [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. | @@ -3148,7 +3148,7 @@ These are the available report types: | [`artifacts:reports:dast`](../pipelines/job_artifacts.md#artifactsreportsdast-ultimate) **(ULTIMATE)** | The `dast` report collects Dynamic Application Security Testing vulnerabilities. | | [`artifacts:reports:license_management`](../pipelines/job_artifacts.md#artifactsreportslicense_management-ultimate) **(ULTIMATE)** | The `license_management` report collects Licenses (*removed from GitLab 13.0*). | | [`artifacts:reports:license_scanning`](../pipelines/job_artifacts.md#artifactsreportslicense_scanning-ultimate) **(ULTIMATE)** | The `license_scanning` report collects Licenses. | -| [`artifacts:reports:performance`](../pipelines/job_artifacts.md#artifactsreportsperformance-premium) **(PREMIUM)** | The `performance` report collects Performance metrics. | +| [`artifacts:reports:performance`](../pipelines/job_artifacts.md#artifactsreportsperformance-premium) **(PREMIUM)** | The `performance` report collects Browser Performance metrics. | | [`artifacts:reports:metrics`](../pipelines/job_artifacts.md#artifactsreportsmetrics-premium) **(PREMIUM)** | The `metrics` report collects Metrics. | #### `dependencies` diff --git a/doc/user/project/merge_requests/browser_performance_testing.md b/doc/user/project/merge_requests/browser_performance_testing.md index 75103dd208e..10457e40e0b 100644 --- a/doc/user/project/merge_requests/browser_performance_testing.md +++ b/doc/user/project/merge_requests/browser_performance_testing.md @@ -10,20 +10,16 @@ type: reference, howto > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3507) in [GitLab Premium](https://about.gitlab.com/pricing/) 10.3. If your application offers a web interface and you're using -[GitLab CI/CD](../../../ci/README.md), you can quickly determine the performance -impact of pending code changes. +[GitLab CI/CD](../../../ci/README.md), you can quickly determine the rendering performance +impact of pending code changes in the browser. ## Overview GitLab uses [Sitespeed.io](https://www.sitespeed.io), a free and open source -tool, for measuring the performance of web sites. GitLab has built a simple -[Sitespeed plugin](https://gitlab.com/gitlab-org/gl-performance) which outputs -the performance score for each page analyzed in a file called `performance.json`. -The [Sitespeed.io performance score](https://examples.sitespeed.io/6.0/2017-11-23-23-43-35/help.html) -is a composite value based on best practices. - -GitLab can [show the Performance report](#how-browser-performance-testing-works) -in the merge request widget area. +tool, for measuring the rendering performance of web sites. The +[Sitespeed plugin](https://gitlab.com/gitlab-org/gl-performance) that GitLab built outputs +the performance score for each page analyzed in a file called `browser-performance.json` +this data can be shown on Merge Requests. ## Use cases @@ -41,7 +37,7 @@ Consider the following workflow: ## How browser performance testing works First, define a job in your `.gitlab-ci.yml` file that generates the -[Performance report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsperformance-premium). +[Browser Performance report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsperformance-premium). GitLab then checks this report, compares key performance metrics for each page between the source and target branches, and shows the information in the merge request. @@ -49,12 +45,13 @@ For an example Performance job, see [Configuring Browser Performance Testing](#configuring-browser-performance-testing). NOTE: **Note:** -If the Performance report has no data to compare, such as when you add the -Performance job in your `.gitlab-ci.yml` for the very first time, no information -displays in the merge request widget area. Consecutive merge requests will have data for -comparison, and the Performance report will be shown properly. +If the Browser Performance report has no data to compare, such as when you add the +Browser Performance job in your `.gitlab-ci.yml` for the very first time, +the Browser Performance report widget won't show. It must have run at least +once on the target branch (`master`, for example), before it will display in a +merge request targeting that branch. -![Performance Widget](img/browser_performance_testing.png) +![Browser Performance Widget](img/browser_performance_testing.png) ## Configuring Browser Performance Testing @@ -64,21 +61,7 @@ using Docker-in-Docker. 1. First, set up GitLab Runner with a [Docker-in-Docker build](../../../ci/docker/using_docker_build.md#use-docker-in-docker-workflow-with-docker-executor). -1. After configuring the Runner, add a new job to `.gitlab-ci.yml` that generates - the expected report. -1. Define the `performance` job according to your version of GitLab: - - - For GitLab 12.4 and later - [include](../../../ci/yaml/README.md#includetemplate) the - [`Browser-Performance.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml) provided as a part of your GitLab installation. - - For GitLab versions earlier than 12.4 - Copy and use the job as defined in the - [`Browser-Performance.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml). - - CAUTION: **Caution:** - The job definition provided by the template does not support Kubernetes yet. - For a complete example of a more complex setup that works in Kubernetes, see - [`Browser-Performance-Testing.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml). - -1. Add the following to your `.gitlab-ci.yml` file: +1. Configure the default Browser Performance Testing CI job as follows in your `.gitlab-ci.yml` file: ```yaml include: @@ -89,24 +72,32 @@ using Docker-in-Docker. URL: https://example.com ``` - CAUTION: **Caution:** - The job definition provided by the template is supported in GitLab 11.5 and later versions. - It also requires GitLab Runner 11.5 or later. For earlier versions, use the - [previous job definitions](#previous-job-definitions). +NOTE: **Note:** +For versions before 12.4, see the information for [older GitLab versions](#gitlab-versions-123-and-older). +If you are using a Kubernetes cluster, use [`template: Jobs/Browser-Performance-Testing.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml) +instead. The above example creates a `performance` job in your CI/CD pipeline and runs sitespeed.io against the webpage you defined in `URL` to gather key metrics. -The [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance) -is downloaded to save the report as a [Performance report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsperformance-premium) -that you can later download and analyze. Due to implementation limitations, we always -take the latest Performance artifact available. -The full HTML sitespeed.io report is saved as an artifact, and if -[GitLab Pages](../pages/index.md) is enabled, it can be viewed directly in your browser. +The example uses a CI/CD template that is included in all GitLab installations since +12.4, but it will not work with Kubernetes clusters. If you are using GitLab 12.3 +or older, you must [add the configuration manually](#gitlab-versions-123-and-older) + +The template uses the [GitLab plugin for sitespeed.io](https://gitlab.com/gitlab-org/gl-performance), +and it saves the full HTML sitespeed.io report as a [Browser Performance report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportsperformance-premium) +that you can later download and analyze. This implementation always takes the latest +Browser Performance artifact available. If [GitLab Pages](../pages/index.md) is enabled, +you can view the report directly in your browser. + +You can also customize the jobs with environment variables: + +- `SITESPEED_IMAGE`: Configure the Docker image to use for the job (default `sitespeedio/sitespeed.io`), but not the image version. +- `SITESPEED_VERSION`: Configure the version of the Docker image to use for the job (default `13.3.0`). +- `SITESPEED_OPTIONS`: Configure any additional sitespeed.io options as required (default `nil`). Refer to the [sitespeed.io documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/) for more details. -You can also customize options by setting the `SITESPEED_OPTIONS` variable. For example, you can override the number of runs sitespeed.io -makes on the given URL: +makes on the given URL, and change the version: ```yaml include: @@ -114,18 +105,11 @@ include: performance: variables: - URL: https://example.com + URL: https://www.sitespeed.io/ + SITESPEED_VERSION: 13.2.0 SITESPEED_OPTIONS: -n 5 ``` -For further customization options for sitespeed.io, including the ability to provide a -list of URLs to test, please see the -[Sitespeed.io Configuration](https://www.sitespeed.io/documentation/sitespeed.io/configuration/) -documentation. - -TIP: **Tip:** -Key metrics are automatically extracted and shown in the merge request widget. - ### Configuring degradation threshold > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27599) in GitLab 13.0. @@ -152,15 +136,12 @@ The above CI YAML configuration is great for testing against static environments be extended for dynamic environments, but a few extra steps are required: 1. The `performance` job should run after the dynamic environment has started. -1. In the `review` job, persist the hostname and upload it as an artifact so - it's available to the `performance` job. The same can be done for static - environments like staging and production to unify the code path. You can save it - as an artifact with `echo $CI_ENVIRONMENT_URL > environment_url.txt` - in your job's `script`. -1. In the `performance` job, read the previous artifact into an environment - variable. In this case, use `$URL` because the sitespeed.io command - uses it for the URL parameter. Because Review App URLs are dynamic, define - the `URL` variable through `before_script` instead of `variables`. +1. In the `review` job: + 1. Generate a URL list file with the dynamic URL. + 1. Save the file as an artifact, for example with `echo $CI_ENVIRONMENT_URL > environment_url.txt` + in your job's `script`. + 1. Pass the list as the URL environment variable (which can be a URL or a file containing URLs) + to the `performance` job. 1. You can now run the sitespeed.io container against the desired hostname and paths. @@ -193,20 +174,21 @@ review: performance: dependencies: - review - before_script: - - export URL=$(cat environment_url.txt) + variables: + URL: environment_url.txt ``` -### Previous job definitions +### GitLab versions 12.3 and older -CAUTION: **Caution:** -Before GitLab 11.5, the Performance job and artifact had to be named specifically -to automatically extract report data and show it in the merge request widget. -While these old job definitions are still maintained, they have been deprecated -and may be removed in next major release, GitLab 12.0. -GitLab recommends you update your current `.gitlab-ci.yml` configuration to reflect that change. +Browser Performance Testing has gone through several changes since it's introduction. +In this section we'll detail these changes and how you can run the test based on your +GitLab version: -For GitLab 11.4 and earlier, the job should look like: +- In GitLab 12.4 [a job template was made available](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml). +- In 13.2 the feature was renamed from `Performance` to `Browser Performance` with +additional template variables. The job name in the template is still `performance` +for compatibility reasons, but may be renamed to match in a future iteration. +- For 11.5 to 12.3 no template is available and the job has to be defined manually as follows: ```yaml performance: @@ -214,28 +196,45 @@ performance: image: docker:git variables: URL: https://example.com + SITESPEED_VERSION: 13.3.0 + SITESPEED_OPTIONS: '' services: - docker:stable-dind script: - mkdir gitlab-exporter - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js - mkdir sitespeed-results - - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL + - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS - mv sitespeed-results/data/performance.json performance.json artifacts: paths: - performance.json - sitespeed-results/ + reports: + performance: performance.json ``` -<!-- ## Troubleshooting +- For 11.4 and earlier the job should be defined as follows: -Include any troubleshooting steps that you can foresee. If you know beforehand what issues -one might have when setting this up, or when something is changed, or on upgrading, it's -important to describe those, too. Think of things that may go wrong and include them here. -This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. +```yaml +performance: + stage: performance + image: docker:git + variables: + URL: https://example.com + services: + - docker:stable-dind + script: + - mkdir gitlab-exporter + - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js + - mkdir sitespeed-results + - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL + - mv sitespeed-results/data/performance.json performance.json + artifacts: + paths: + - performance.json + - sitespeed-results/ +``` -Each scenario can be a third-level heading, e.g. `### Getting error message X`. -If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. --> +Upgrading to the latest version and using the templates is recommended, to ensure +you receive the latest updates, including updates to the sitespeed.io versions. diff --git a/doc/user/project/merge_requests/img/browser_performance_testing.png b/doc/user/project/merge_requests/img/browser_performance_testing.png Binary files differindex eea77fb8b93..c270462f7a8 100644 --- a/doc/user/project/merge_requests/img/browser_performance_testing.png +++ b/doc/user/project/merge_requests/img/browser_performance_testing.png diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 1974d81df58..a614d0a61d4 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -44,6 +44,8 @@ Note the following: ## Version history +### 13.0+ + Starting with GitLab 13.0, GitLab can import bundles that were exported from a different GitLab deployment. This ability is limited to two previous GitLab [minor](../../../policy/maintenance.md#versioning) releases, which is similar to our process for [Security Releases](../../../policy/maintenance.md#security-releases). @@ -61,7 +63,7 @@ Prior to 13.0 this was a defined compatibility table: | Exporting GitLab version | Importing GitLab version | | -------------------------- | -------------------------- | -| 11.7 to 13.0 | 11.7 to 13.0 | +| 11.7 to 12.10 | 11.7 to 12.10 | | 11.1 to 11.6 | 11.1 to 11.6 | | 10.8 to 11.0 | 10.8 to 11.0 | | 10.4 to 10.7 | 10.4 to 10.7 | diff --git a/doc/user/shortcuts.md b/doc/user/shortcuts.md index efa374cf1c3..314fe367ca6 100644 --- a/doc/user/shortcuts.md +++ b/doc/user/shortcuts.md @@ -78,10 +78,11 @@ These shortcuts are available when viewing issues and merge requests. | <kbd>m</kbd> | Change milestone. | | <kbd>l</kbd> | Change label. | | <kbd>r</kbd> | Start writing a comment. If any text is selected, it will be quoted in the comment. Can't be used to reply within a thread. | -| <kbd>n</kbd> | Move to next unresolved discussion (Merge requests only). | -| <kbd>p</kbd> | Move to previous unresolved discussion (Merge requests only). | -| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file (Merge requests only). | -| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file (Merge requests only). | +| <kbd>n</kbd> | Move to next unresolved discussion (merge requests only). | +| <kbd>p</kbd> | Move to previous unresolved discussion (merge requests only). | +| <kbd>]</kbd> or <kbd>j</kbd> | Move to next file (merge requests only). | +| <kbd>[</kbd> or <kbd>k</kbd> | Move to previous file (merge requests only). | +| <kbd>b</kbd> | Copy source branch name (merge requests only). | ### Project Files diff --git a/lib/api/entities/basic_project_details.rb b/lib/api/entities/basic_project_details.rb index 13bc19456b3..cf0b32bed26 100644 --- a/lib/api/entities/basic_project_details.rb +++ b/lib/api/entities/basic_project_details.rb @@ -33,7 +33,8 @@ module API project.avatar_url(only_path: false) end - expose :star_count, :forks_count + expose :forks_count + expose :star_count expose :last_activity_at expose :namespace, using: 'API::Entities::NamespaceBasic' expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes diff --git a/lib/api/entities/group_detail.rb b/lib/api/entities/group_detail.rb index 93dc41da81d..2d9d4ca7992 100644 --- a/lib/api/entities/group_detail.rb +++ b/lib/api/entities/group_detail.rb @@ -7,6 +7,7 @@ module API SharedGroupWithGroup.represent(group.shared_with_group_links.public_or_visible_to_user(group, options[:current_user])) end expose :runners_token, if: lambda { |group, options| options[:user_can_admin_group] } + expose :projects, using: Entities::Project do |group, options| projects = GroupProjectsFinder.new( group: group, diff --git a/lib/api/projects_relation_builder.rb b/lib/api/projects_relation_builder.rb index 263468c9aa6..6dfd82d109f 100644 --- a/lib/api/projects_relation_builder.rb +++ b/lib/api/projects_relation_builder.rb @@ -8,6 +8,10 @@ module API def prepare_relation(projects_relation, options = {}) projects_relation = preload_relation(projects_relation, options) execute_batch_counting(projects_relation) + # Call the forks count method on every project, so the BatchLoader would load them all at + # once when the entities are rendered + projects_relation.each(&:forks_count) + projects_relation end @@ -19,16 +23,11 @@ module API projects_relation end - def batch_forks_counting(projects_relation) - ::Projects::BatchForksCountService.new(forks_counting_projects(projects_relation)).refresh_cache - end - def batch_open_issues_counting(projects_relation) ::Projects::BatchOpenIssuesCountService.new(projects_relation).refresh_cache end def execute_batch_counting(projects_relation) - batch_forks_counting(projects_relation) batch_open_issues_counting(projects_relation) end end diff --git a/lib/gitlab/ci/config/entry/release.rb b/lib/gitlab/ci/config/entry/release.rb index d3dfb49bf34..7e504c24ade 100644 --- a/lib/gitlab/ci/config/entry/release.rb +++ b/lib/gitlab/ci/config/entry/release.rb @@ -44,10 +44,10 @@ module Gitlab end validate do next unless config[:ref] + next if Commit.reference_valid?(config[:ref]) + next if Gitlab::GitRefValidator.validate(config[:ref]) - unless Commit.reference_valid?(config[:ref]) - errors.add(:ref, "must be a valid ref") - end + errors.add(:ref, "must be a valid ref") end end diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb index 38dfea2a0c1..8be18c059a3 100644 --- a/lib/gitlab/ci/config/entry/reports.rb +++ b/lib/gitlab/ci/config/entry/reports.rb @@ -13,7 +13,7 @@ module Gitlab ALLOWED_KEYS = %i[junit codequality sast secret_detection dependency_scanning container_scanning - dast performance license_management license_scanning metrics lsif + dast performance browser_performance license_management license_scanning metrics lsif dotenv cobertura terraform accessibility cluster_applications requirements coverage_fuzzing].freeze @@ -33,6 +33,7 @@ module Gitlab validates :container_scanning, array_of_strings_or_string: true validates :dast, array_of_strings_or_string: true validates :performance, array_of_strings_or_string: true + validates :browser_performance, array_of_strings_or_string: true validates :license_management, array_of_strings_or_string: true validates :license_scanning, array_of_strings_or_string: true validates :metrics, array_of_strings_or_string: true diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb index 8bbf2e0f6cf..28b81e7a471 100644 --- a/lib/gitlab/ci/reports/test_suite.rb +++ b/lib/gitlab/ci/reports/test_suite.rb @@ -4,9 +4,9 @@ module Gitlab module Ci module Reports class TestSuite - attr_reader :name - attr_reader :test_cases - attr_reader :total_time + attr_accessor :name + attr_accessor :test_cases + attr_accessor :total_time attr_reader :suite_error def initialize(name = nil) @@ -70,6 +70,14 @@ module Gitlab @suite_error = msg end + def +(other) + self.class.new.tap do |test_suite| + test_suite.name = self.name + test_suite.test_cases = self.test_cases.deep_merge(other.test_cases) + test_suite.total_time = self.total_time + other.total_time + end + end + private def existing_key?(test_case) diff --git a/lib/gitlab/ci/reports/test_suite_summary.rb b/lib/gitlab/ci/reports/test_suite_summary.rb index 707b443a113..f9b0bedb712 100644 --- a/lib/gitlab/ci/reports/test_suite_summary.rb +++ b/lib/gitlab/ci/reports/test_suite_summary.rb @@ -15,6 +15,10 @@ module Gitlab end # rubocop: disable CodeReuse/ActiveRecord + def build_ids + results.pluck(:build_id) + end + def total_time @total_time ||= results.sum(&:tests_duration) end diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml index d4ac8036594..44d71ef13b1 100644 --- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml @@ -1,10 +1,14 @@ +# Read more about the feature here: https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html + performance: stage: performance image: docker:19.03.11 allow_failure: true variables: DOCKER_TLS_CERTDIR: "" - SITESPEED_IMAGE: "sitespeedio/sitespeed.io:11.2.0" + SITESPEED_IMAGE: sitespeedio/sitespeed.io + SITESPEED_VERSION: 13.3.0 + SITESPEED_OPTIONS: '' services: - docker:19.03.11-dind script: @@ -16,22 +20,22 @@ performance: fi - export CI_ENVIRONMENT_URL=$(cat environment_url.txt) - mkdir gitlab-exporter - - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.0.0/index.js + - wget -O gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/1.0.1/index.js - mkdir sitespeed-results - - docker pull --quiet ${SITESPEED_IMAGE} - | if [ -f .gitlab-urls.txt ] then sed -i -e 's@^@'"$CI_ENVIRONMENT_URL"'@' .gitlab-urls.txt - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io ${SITESPEED_IMAGE} --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results .gitlab-urls.txt $SITESPEED_OPTIONS else - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io ${SITESPEED_IMAGE} --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" + docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" $SITESPEED_OPTIONS fi - - mv sitespeed-results/data/performance.json performance.json + - mv sitespeed-results/data/performance.json browser-performance.json artifacts: paths: - - performance.json - sitespeed-results/ + reports: + browser_performance: browser-performance.json rules: - if: '$CI_KUBERNETES_ACTIVE == null || $CI_KUBERNETES_ACTIVE == ""' when: never diff --git a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml index e6097ae322e..9dbd9b679a8 100644 --- a/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml @@ -10,8 +10,9 @@ performance: stage: performance image: docker:git variables: - URL: https://example.com - SITESPEED_VERSION: 11.2.0 + URL: '' + SITESPEED_IMAGE: sitespeedio/sitespeed.io + SITESPEED_VERSION: 13.3.0 SITESPEED_OPTIONS: '' services: - docker:stable-dind @@ -19,11 +20,10 @@ performance: - mkdir gitlab-exporter - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js - mkdir sitespeed-results - - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS - - mv sitespeed-results/data/performance.json performance.json + - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io $SITESPEED_IMAGE:$SITESPEED_VERSION --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL $SITESPEED_OPTIONS + - mv sitespeed-results/data/performance.json browser-performance.json artifacts: paths: - - performance.json - sitespeed-results/ reports: - performance: performance.json + browser_performance: browser-performance.json diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 72ac6b855f4..343119acc62 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2316,6 +2316,9 @@ msgstr "" msgid "Also called \"Relying party service URL\" or \"Reply URL\"" msgstr "" +msgid "Also unassign this user from related issues and merge requests" +msgstr "" + msgid "Alternate support URL for help page and help dropdown" msgstr "" @@ -6590,6 +6593,9 @@ msgstr "" msgid "Copy secret" msgstr "" +msgid "Copy source branch name" +msgstr "" + msgid "Copy token" msgstr "" @@ -7519,6 +7525,9 @@ msgstr "" msgid "Deny" msgstr "" +msgid "Deny access request" +msgstr "" + msgid "Dependencies" msgstr "" @@ -19186,6 +19195,9 @@ msgstr "" msgid "Remove limit" msgstr "" +msgid "Remove member" +msgstr "" + msgid "Remove milestone" msgstr "" @@ -24452,6 +24464,9 @@ msgstr "" msgid "Total Contributions" msgstr "" +msgid "Total Score" +msgstr "" + msgid "Total artifacts size: %{total_size}" msgstr "" @@ -27082,6 +27097,12 @@ msgstr "" msgid "cannot merge" msgstr "" +msgid "ciReport|%{degradedNum} degraded" +msgstr "" + +msgid "ciReport|%{improvedNum} improved" +msgstr "" + msgid "ciReport|%{linkStartTag}Learn more about Container Scanning %{linkEndTag}" msgstr "" @@ -27109,6 +27130,9 @@ msgstr "" msgid "ciReport|%{reportType}: Loading resulted in an error" msgstr "" +msgid "ciReport|%{sameNum} same" +msgstr "" + msgid "ciReport|(errors when loading results)" msgstr "" @@ -27133,6 +27157,12 @@ msgstr "" msgid "ciReport|Base pipeline codequality artifact not found" msgstr "" +msgid "ciReport|Browser performance test metrics: " +msgstr "" + +msgid "ciReport|Browser performance test metrics: No changes" +msgstr "" + msgid "ciReport|Code quality" msgstr "" @@ -27205,15 +27235,9 @@ msgstr "" msgid "ciReport|No changes to code quality" msgstr "" -msgid "ciReport|No changes to performance metrics" -msgstr "" - msgid "ciReport|No code quality issues found" msgstr "" -msgid "ciReport|Performance metrics" -msgstr "" - msgid "ciReport|Resolve with merge request" msgstr "" diff --git a/spec/controllers/projects/pipelines/tests_controller_spec.rb b/spec/controllers/projects/pipelines/tests_controller_spec.rb index 5be4e19f9f9..e2abd1238c5 100644 --- a/spec/controllers/projects/pipelines/tests_controller_spec.rb +++ b/spec/controllers/projects/pipelines/tests_controller_spec.rb @@ -46,12 +46,66 @@ RSpec.describe Projects::Pipelines::TestsController do end end + describe 'GET #show.json' do + context 'when pipeline has build report results' do + let(:pipeline) { create(:ci_pipeline, :with_report_results, project: project) } + let(:suite_name) { 'test' } + let(:build_ids) { pipeline.latest_builds.pluck(:id) } + + it 'renders test suite data' do + get_tests_show_json(build_ids) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['name']).to eq('test') + end + end + + context 'when pipeline does not have build report results' do + let(:pipeline) { create(:ci_empty_pipeline) } + let(:suite_name) { 'test' } + + it 'renders 404' do + get_tests_show_json([]) + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to be_empty + end + end + + context 'when feature is disabled' do + let(:suite_name) { 'test' } + + before do + stub_feature_flags(build_report_summary: false) + end + + it 'returns 404' do + get_tests_show_json([]) + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).to be_empty + end + end + end + def get_tests_summary_json get :summary, params: { namespace_id: project.namespace, project_id: project, - id: pipeline.id + pipeline_id: pipeline.id + }, + format: :json + end + + def get_tests_show_json(build_ids) + get :show, + params: { + namespace_id: project.namespace, + project_id: project, + pipeline_id: pipeline.id, + suite_name: suite_name, + build_ids: build_ids }, format: :json end diff --git a/spec/factories/product_analytics_event.rb b/spec/factories/product_analytics_event.rb new file mode 100644 index 00000000000..168b255f6ca --- /dev/null +++ b/spec/factories/product_analytics_event.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :product_analytics_event do + project + platform { 'web' } + collector_tstamp { DateTime.now } + dvce_created_tstamp { DateTime.now } + event_id { SecureRandom.uuid } + name_tracker { 'sp' } + v_tracker { 'js-2.14.0' } + v_collector { 'GitLab 13.1.0-pre' } + v_etl { 'GitLab 13.1.0-pre' } + domain_userid { SecureRandom.uuid } + domain_sessionidx { 4 } + page_url { 'http://localhost:3333/products/123' } + br_lang { 'en-US' } + br_cookies { true } + br_colordepth { '24' } + os_timezone { 'America/Los_Angeles' } + doc_charset { 'UTF-8' } + domain_sessionid { SecureRandom.uuid } + end +end diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb index e29d8fd651e..99846ecee27 100644 --- a/spec/features/groups/members/manage_members_spec.rb +++ b/spec/features/groups/members/manage_members_spec.rb @@ -68,9 +68,12 @@ RSpec.describe 'Groups > Members > Manage members' do visit group_group_members_path(group) - accept_confirm do - find(:css, '.project-members-page li', text: user2.name).find(:css, 'a.btn-remove').click - end + # Open modal + find(:css, '.project-members-page li', text: user2.name).find(:css, 'button.btn-remove').click + + expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests' + + click_on('Remove member') wait_for_requests diff --git a/spec/features/projects/members/list_spec.rb b/spec/features/projects/members/list_spec.rb index f51ebde8f80..56b807e08d7 100644 --- a/spec/features/projects/members/list_spec.rb +++ b/spec/features/projects/members/list_spec.rb @@ -64,9 +64,12 @@ RSpec.describe 'Project members list' do visit_members_page - accept_confirm do - find(:css, 'li.project_member', text: other_user.name).find(:css, 'a.btn-remove').click - end + # Open modal + find(:css, 'li.project_member', text: other_user.name).find(:css, 'button.btn-remove').click + + expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests' + + click_on('Remove member') wait_for_requests diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb index d32f4cb8ec7..3836b95a28a 100644 --- a/spec/features/projects/settings/user_manages_project_members_spec.rb +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -16,15 +16,20 @@ RSpec.describe 'Projects > Settings > User manages project members' do sign_in(user) end - it 'cancels a team member' do + it 'cancels a team member', :js do visit(project_project_members_path(project)) project_member = project.project_members.find_by(user_id: user_dmitriy.id) page.within("#project_member_#{project_member.id}") do - click_link('Remove user from project') + # Open modal + click_on('Remove user from project') end + expect(page).to have_unchecked_field 'Also unassign this user from related issues and merge requests' + + click_on('Remove member') + visit(project_project_members_path(project)) expect(page).not_to have_content(user_dmitriy.name) diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js index 2e52958a828..1aaae80dcdf 100644 --- a/spec/frontend/lib/utils/text_markdown_spec.js +++ b/spec/frontend/lib/utils/text_markdown_spec.js @@ -1,4 +1,4 @@ -import { insertMarkdownText } from '~/lib/utils/text_markdown'; +import { insertMarkdownText, keypressNoteText } from '~/lib/utils/text_markdown'; describe('init markdown', () => { let textArea; @@ -115,14 +115,15 @@ describe('init markdown', () => { describe('with selection', () => { const text = 'initial selected value'; const selected = 'selected'; + let selectedIndex; + beforeEach(() => { textArea.value = text; - const selectedIndex = text.indexOf(selected); + selectedIndex = text.indexOf(selected); textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); }); it('applies the tag to the selected value', () => { - const selectedIndex = text.indexOf(selected); const tag = '*'; insertMarkdownText({ @@ -153,6 +154,29 @@ describe('init markdown', () => { expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`)); }); + it.each` + key | expected + ${'['} | ${`[${selected}]`} + ${'*'} | ${`**${selected}**`} + ${"'"} | ${`'${selected}'`} + ${'_'} | ${`_${selected}_`} + ${'`'} | ${`\`${selected}\``} + ${'"'} | ${`"${selected}"`} + ${'{'} | ${`{${selected}}`} + ${'('} | ${`(${selected})`} + ${'<'} | ${`<${selected}>`} + `('generates $expected when $key is pressed', ({ key, expected }) => { + const event = new KeyboardEvent('keydown', { key }); + + textArea.addEventListener('keydown', keypressNoteText); + textArea.dispatchEvent(event); + + expect(textArea.value).toEqual(text.replace(selected, expected)); + + // cursor placement should be after selection + 2 tag lengths + expect(textArea.selectionStart).toBe(selectedIndex + expected.length); + }); + describe('and text to be selected', () => { const tag = '[{text}](url)'; const select = 'url'; @@ -178,7 +202,7 @@ describe('init markdown', () => { it('selects the right text when multiple tags are present', () => { const initialValue = `${tag} ${tag} ${selected}`; textArea.value = initialValue; - const selectedIndex = initialValue.indexOf(selected); + selectedIndex = initialValue.indexOf(selected); textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length); insertMarkdownText({ textArea, @@ -204,7 +228,7 @@ describe('init markdown', () => { const initialValue = `text ${expectedUrl} text`; textArea.value = initialValue; - const selectedIndex = initialValue.indexOf(expectedUrl); + selectedIndex = initialValue.indexOf(expectedUrl); textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length); insertMarkdownText({ diff --git a/spec/frontend/releases/components/app_new_spec.js b/spec/frontend/releases/components/app_new_spec.js new file mode 100644 index 00000000000..0d5664766e5 --- /dev/null +++ b/spec/frontend/releases/components/app_new_spec.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import { mount } from '@vue/test-utils'; +import ReleaseNewApp from '~/releases/components/app_new.vue'; + +Vue.use(Vuex); + +describe('Release new component', () => { + let wrapper; + + const factory = () => { + const store = new Vuex.Store(); + wrapper = mount(ReleaseNewApp, { store }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders the app', () => { + factory(); + + expect(wrapper.exists()).toBe(true); + }); +}); diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js index b492a69fb3d..21058005d29 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js @@ -1,7 +1,13 @@ import Vue from 'vue'; +import Mousetrap from 'mousetrap'; import mountComponent from 'helpers/vue_mount_component_helper'; import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue'; +jest.mock('mousetrap', () => ({ + bind: jest.fn(), + unbind: jest.fn(), +})); + describe('MRWidgetHeader', () => { let vm; let Component; @@ -126,6 +132,35 @@ describe('MRWidgetHeader', () => { it('renders target branch', () => { expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master'); }); + + describe('keyboard shortcuts', () => { + it('binds a keyboard shortcut handler to the "b" key', () => { + expect(Mousetrap.bind).toHaveBeenCalledWith('b', expect.any(Function)); + }); + + it('triggers a click on the "copy to clipboard" button when the handler is executed', () => { + const testClickHandler = jest.fn(); + vm.$refs.copyBranchNameButton.$el.addEventListener('click', testClickHandler); + + // Get a reference to the function that was assigned to the "b" shortcut key. + const shortcutHandler = Mousetrap.bind.mock.calls[0][1]; + + expect(testClickHandler).not.toHaveBeenCalled(); + + // Simulate Mousetrap calling the function. + shortcutHandler(); + + expect(testClickHandler).toHaveBeenCalledTimes(1); + }); + + it('unbinds the keyboard shortcut when the component is destroyed', () => { + expect(Mousetrap.unbind).not.toHaveBeenCalled(); + + vm.$destroy(); + + expect(Mousetrap.unbind).toHaveBeenCalledWith('b'); + }); + }); }); describe('with an open merge request', () => { diff --git a/spec/frontend/vue_shared/components/remove_member_modal_spec.js b/spec/frontend/vue_shared/components/remove_member_modal_spec.js new file mode 100644 index 00000000000..2d380b25a0a --- /dev/null +++ b/spec/frontend/vue_shared/components/remove_member_modal_spec.js @@ -0,0 +1,65 @@ +import { GlFormCheckbox, GlModal } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; + +describe('RemoveMemberModal', () => { + const memberPath = '/gitlab-org/gitlab-test/-/project_members/90'; + let wrapper; + + const findForm = () => wrapper.find({ ref: 'form' }); + const findGlModal = () => wrapper.find(GlModal); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe.each` + state | isAccessRequest | actionText | checkboxTestDescription | checkboxExpected | message + ${'removing a member'} | ${'false'} | ${'Remove member'} | ${'shows a checkbox to allow removal from related issues and MRs'} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} + ${'denying an access request'} | ${'true'} | ${'Deny access request'} | ${'does not show a checkbox'} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} + `( + 'when $state', + ({ actionText, isAccessRequest, message, checkboxTestDescription, checkboxExpected }) => { + beforeEach(() => { + wrapper = shallowMount(RemoveMemberModal, { + data() { + return { + modalData: { + isAccessRequest, + message, + memberPath, + }, + }; + }, + }); + }); + + it(`has the title ${actionText}`, () => { + expect(findGlModal().attributes('title')).toBe(actionText); + }); + + it('contains a form action', () => { + expect(findForm().attributes('action')).toBe(memberPath); + }); + + it('displays a message to the user', () => { + expect(wrapper.find('[data-testid=modal-message]').text()).toBe(message); + }); + + it(`${checkboxTestDescription}`, () => { + expect(wrapper.contains(GlFormCheckbox)).toBe(checkboxExpected); + }); + + it('submits the form when the modal is submitted', () => { + const spy = jest.spyOn(findForm().element, 'submit'); + + findGlModal().vm.$emit('primary'); + + expect(spy).toHaveBeenCalled(); + + spy.mockRestore(); + }); + }, + ); +}); diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index 8096cd001c0..17db6648092 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -12,7 +12,7 @@ RSpec.describe GitlabSchema.types['Issue'] do specify { expect(described_class.interfaces).to include(Types::Notes::NoteableType) } it 'has specific fields' do - fields = %i[iid title description state reference author assignees participants labels milestone due_date + fields = %i[id iid title description state reference author assignees participants labels milestone due_date confidential discussion_locked upvotes downvotes user_notes_count web_path web_url relative_position subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status designs design_collection] diff --git a/spec/helpers/releases_helper_spec.rb b/spec/helpers/releases_helper_spec.rb index b84560fc45b..82fc799f9b0 100644 --- a/spec/helpers/releases_helper_spec.rb +++ b/spec/helpers/releases_helper_spec.rb @@ -22,6 +22,7 @@ RSpec.describe ReleasesHelper do let(:can_user_create_release) { false } let(:common_keys) { [:project_id, :illustration_path, :documentation_path] } + # rubocop: disable CodeReuse/ActiveRecord before do helper.instance_variable_set(:@project, project) helper.instance_variable_set(:@release, release) @@ -30,6 +31,7 @@ RSpec.describe ReleasesHelper do .with(user, :create_release, project) .and_return(can_user_create_release) end + # rubocop: enable CodeReuse/ActiveRecord describe '#data_for_releases_page' do it 'includes the required data for displaying release blocks' do @@ -41,7 +43,20 @@ RSpec.describe ReleasesHelper do it 'includes new_release_path' do expect(helper.data_for_releases_page.keys).to contain_exactly(*common_keys, :new_release_path) - expect(helper.data_for_releases_page[:new_release_path]).to eq(new_project_tag_path(project)) + end + + it 'points new_release_path to the "New Release" page' do + expect(helper.data_for_releases_page[:new_release_path]).to eq(new_project_release_path(project)) + end + + context 'when the "new_release_page" feature flag is disabled' do + before do + stub_feature_flags(new_release_page: false) + end + + it 'points new_release_path to the "New Tag" page' do + expect(helper.data_for_releases_page[:new_release_path]).to eq(new_project_tag_path(project)) + end end end end @@ -57,7 +72,23 @@ RSpec.describe ReleasesHelper do release_assets_docs_path manage_milestones_path new_milestone_path) - expect(helper.data_for_edit_release_page.keys).to eq(keys) + + expect(helper.data_for_edit_release_page.keys).to match_array(keys) + end + end + + describe '#data_for_new_release_page' do + it 'has the needed data to display the "new release" page' do + keys = %i(project_id + markdown_preview_path + markdown_docs_path + update_release_api_docs_path + release_assets_docs_path + manage_milestones_path + new_milestone_path + default_branch) + + expect(helper.data_for_new_release_page.keys).to match_array(keys) end end end diff --git a/spec/lib/gitlab/ci/config/entry/release_spec.rb b/spec/lib/gitlab/ci/config/entry/release_spec.rb index 790ed160d15..e5155f91be4 100644 --- a/spec/lib/gitlab/ci/config/entry/release_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/release_spec.rb @@ -117,11 +117,39 @@ RSpec.describe Gitlab::Ci::Config::Entry::Release do tag_name: 'v0.06', description: "./release_changelog.txt", name: "Release $CI_TAG_NAME", - ref: 'b3235930aa443112e639f941c69c578912189bdd' + ref: ref } end - it_behaves_like 'a valid entry' + context "when 'ref' is a full commit SHA" do + let(:ref) { 'b3235930aa443112e639f941c69c578912189bdd' } + + it_behaves_like 'a valid entry' + end + + context "when 'ref' is a short commit SHA" do + let(:ref) { 'b3235930'} + + it_behaves_like 'a valid entry' + end + + context "when 'ref' is a branch name" do + let(:ref) { 'fix/123-branch-name'} + + it_behaves_like 'a valid entry' + end + + context "when 'ref' is a semantic versioning tag" do + let(:ref) { 'v1.2.3'} + + it_behaves_like 'a valid entry' + end + + context "when 'ref' is a semantic versioning tag rc" do + let(:ref) { 'v1.2.3-rc'} + + it_behaves_like 'a valid entry' + end end context "when value includes 'released_at' keyword" do @@ -193,25 +221,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Release do end context 'when `ref` is not valid' do - let(:config) { { ref: 'ABC123' } } - - it_behaves_like 'reports error', 'release ref must be a valid ref' - end - - context 'when `milestones` is not an array of strings' do - let(:config) { { milestones: [1, 2, 3] } } - - it_behaves_like 'reports error', 'release milestones should be an array of strings or a string' - end - - context 'when `released_at` is not a valid date' do - let(:config) { { released_at: 'ABC123' } } - - it_behaves_like 'reports error', 'release released at must be a valid datetime' - end - - context 'when `ref` is not valid' do - let(:config) { { ref: 'ABC123' } } + let(:config) { { ref: 'invalid\branch' } } it_behaves_like 'reports error', 'release ref must be a valid ref' end diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 8afa4fed52f..3a6e228ce97 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -44,6 +44,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do :license_management | 'gl-license-management-report.json' :license_scanning | 'gl-license-scanning-report.json' :performance | 'performance.json' + :browser_performance | 'browser-performance.json' + :browser_performance | 'performance.json' :lsif | 'lsif.json' :dotenv | 'build.dotenv' :cobertura | 'cobertura-coverage.xml' diff --git a/spec/lib/gitlab/ci/reports/test_suite_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_spec.rb index 94915b41a81..c4c4d2c3704 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_spec.rb @@ -139,6 +139,41 @@ RSpec.describe Gitlab::Ci::Reports::TestSuite do end end + describe '#+' do + let(:test_suite_2) { described_class.new('Rspec') } + + subject { test_suite + test_suite_2 } + + context 'when adding multiple suites together' do + before do + test_suite.add_test_case(test_case_success) + test_suite.add_test_case(test_case_failed) + end + + it 'returns a new test suite' do + expect(subject).to be_an_instance_of(described_class) + end + + it 'returns the suite name' do + expect(subject.name).to eq('Rspec') + end + + it 'returns the sum for total_time' do + expect(subject.total_time).to eq(3.33) + end + + it 'merges tests cases hash', :aggregate_failures do + test_suite_2.add_test_case(create_test_case_java_success) + + failed_keys = test_suite.test_cases['failed'].keys + success_keys = test_suite.test_cases['success'].keys + test_suite_2.test_cases['success'].keys + + expect(subject.test_cases['failed'].keys).to contain_exactly(*failed_keys) + expect(subject.test_cases['success'].keys).to contain_exactly(*success_keys) + end + end + end + Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status_type| describe "##{status_type}" do subject { test_suite.public_send("#{status_type}") } diff --git a/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb b/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb index b2192e24513..12c96acdcf3 100644 --- a/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb +++ b/spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb @@ -17,6 +17,16 @@ RSpec.describe Gitlab::Ci::Reports::TestSuiteSummary do end end + describe '#build_ids' do + subject { test_suite_summary.build_ids } + + context 'when test suite summary has several build report results' do + it 'returns the build ids' do + expect(subject).to contain_exactly(build_report_result_1.build_id, build_report_result_2.build_id) + end + end + end + describe '#total_time' do subject { test_suite_summary.total_time } diff --git a/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb deleted file mode 100644 index 5e5f28b2344..00000000000 --- a/spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Jobs/Browser-Performance-Testing.gitlab-ci.yml' do - subject(:template) do - <<~YAML - stages: - - test - - performance - - include: - - template: 'Jobs/Browser-Performance-Testing.gitlab-ci.yml' - - placeholder: - script: - - keep pipeline validator happy by having a job when stages are intentionally empty - YAML - end - - describe 'the created pipeline' do - let(:user) { create(:admin) } - let(:project) do - create(:project, :repository, variables: [ - build(:ci_variable, key: 'CI_KUBERNETES_ACTIVE', value: 'true') - ]) - end - - let(:default_branch) { 'master' } - let(:pipeline_ref) { default_branch } - let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } - let(:pipeline) { service.execute!(:push) } - let(:build_names) { pipeline.builds.pluck(:name) } - - before do - stub_ci_pipeline_yaml_file(template) - - allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) - allow(project).to receive(:default_branch).and_return(default_branch) - end - - it 'has no errors' do - expect(pipeline.errors).to be_empty - end - - shared_examples_for 'performance job on tag or branch' do - it 'by default' do - expect(build_names).to include('performance') - end - - it 'when PERFORMANCE_DISABLED' do - create(:ci_variable, project: project, key: 'PERFORMANCE_DISABLED', value: '1') - - expect(build_names).not_to include('performance') - end - end - - context 'on master' do - it_behaves_like 'performance job on tag or branch' - end - - context 'on another branch' do - let(:pipeline_ref) { 'feature' } - - it_behaves_like 'performance job on tag or branch' - end - - context 'on tag' do - let(:pipeline_ref) { 'v1.0.0' } - - it_behaves_like 'performance job on tag or branch' - end - - context 'on merge request' do - let(:service) { MergeRequests::CreatePipelineService.new(project, user) } - let(:merge_request) { create(:merge_request, :simple, source_project: project) } - let(:pipeline) { service.execute(merge_request) } - - it 'has no jobs' do - expect(pipeline).to be_merge_request_event - expect(build_names).to be_empty - end - end - end -end diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb index 18dd3ca7951..4b67d4f2964 100644 --- a/spec/models/plan_limits_spec.rb +++ b/spec/models/plan_limits_spec.rb @@ -190,6 +190,7 @@ RSpec.describe PlanLimits do ci_max_artifact_size_license_management ci_max_artifact_size_license_scanning ci_max_artifact_size_performance + ci_max_artifact_size_browser_performance ci_max_artifact_size_metrics ci_max_artifact_size_metrics_referee ci_max_artifact_size_network_referee diff --git a/spec/models/product_analytics_event_spec.rb b/spec/models/product_analytics_event_spec.rb new file mode 100644 index 00000000000..6593edae8ac --- /dev/null +++ b/spec/models/product_analytics_event_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe ProductAnalyticsEvent, type: :model do + it { is_expected.to belong_to(:project) } + it { expect(described_class).to respond_to(:order_by_time) } + + describe '.timerange' do + let_it_be(:event_1) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 1.day) } + let_it_be(:event_2) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 5.days) } + let_it_be(:event_3) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 15.days) } + + it { expect(described_class.timerange(3.days)).to match_array([event_1]) } + it { expect(described_class.timerange(7.days)).to match_array([event_1, event_2]) } + it { expect(described_class.timerange(30.days)).to match_array([event_1, event_2, event_3]) } + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 75336446c7e..d8dfeff665d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -4116,7 +4116,7 @@ RSpec.describe Project do it 'returns the number of forks' do project = build(:project) - expect_any_instance_of(Projects::ForksCountService).to receive(:count).and_return(1) + expect_any_instance_of(::Projects::BatchForksCountService).to receive(:refresh_cache_and_retrieve_data).and_return({ project => 1 }) expect(project.forks_count).to eq(1) end diff --git a/spec/serializers/test_report_summary_entity_spec.rb b/spec/serializers/test_report_summary_entity_spec.rb new file mode 100644 index 00000000000..fcac9af5c23 --- /dev/null +++ b/spec/serializers/test_report_summary_entity_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe TestReportSummaryEntity do + let(:pipeline) { create(:ci_pipeline, :with_report_results) } + let(:entity) { described_class.new(pipeline.test_report_summary) } + + describe '#as_json' do + subject(:as_json) { entity.as_json } + + it 'contains the total time' do + expect(as_json).to include(:total_time) + end + + it 'contains the counts' do + expect(as_json).to include(:total_count, :success_count, :failed_count, :skipped_count, :error_count) + end + + context 'when summary has test suites' do + it 'contains the test suites' do + expect(as_json).to include(:test_suites) + expect(as_json[:test_suites].count).to eq(1) + end + + it 'contains build_ids' do + expect(as_json[:test_suites].first).to include(:build_ids) + end + end + end +end diff --git a/spec/serializers/test_suite_summary_entity_spec.rb b/spec/serializers/test_suite_summary_entity_spec.rb new file mode 100644 index 00000000000..d26592bc60e --- /dev/null +++ b/spec/serializers/test_suite_summary_entity_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe TestSuiteSummaryEntity do + let(:pipeline) { create(:ci_pipeline, :with_report_results) } + let(:entity) { described_class.new(pipeline.test_report_summary.total) } + + describe '#as_json' do + subject(:as_json) { entity.as_json } + + it 'contains the total time' do + expect(as_json).to include(:total_time) + end + + it 'contains the counts' do + expect(as_json).to include(:total_count, :success_count, :failed_count, :skipped_count, :error_count) + end + + it 'contains the build_ids' do + expect(as_json).to include(:build_ids) + end + end +end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index bf26be57980..690b6c7b9d4 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -33,8 +33,8 @@ RSpec.describe Ci::RetryBuildService do job_artifacts_sast job_artifacts_secret_detection job_artifacts_dependency_scanning job_artifacts_container_scanning job_artifacts_dast job_artifacts_license_management job_artifacts_license_scanning - job_artifacts_performance job_artifacts_lsif - job_artifacts_terraform job_artifacts_cluster_applications + job_artifacts_performance job_artifacts_browser_performance + job_artifacts_lsif job_artifacts_terraform job_artifacts_cluster_applications job_artifacts_codequality job_artifacts_metrics scheduled_at job_variables waiting_for_resource_at job_artifacts_metrics_referee job_artifacts_network_referee job_artifacts_dotenv diff --git a/spec/services/jira/jql_builder_service_spec.rb b/spec/services/jira/jql_builder_service_spec.rb index 310ba1a43fd..f51dec18094 100644 --- a/spec/services/jira/jql_builder_service_spec.rb +++ b/spec/services/jira/jql_builder_service_spec.rb @@ -54,6 +54,30 @@ RSpec.describe Jira::JqlBuilderService do end end + context 'with status param' do + let(:params) { { status: "\"'try\"some'more\"quote'here\"" } } + + it 'builds jql' do + expect(subject).to eq("project = PROJECT_KEY AND status = \"\\\"'try\\\"some'more\\\"quote'here\\\"\" order by created DESC") + end + end + + context 'with author_username param' do + let(:params) { { author_username: "\"'try\"some'more\"quote'here\"" } } + + it 'builds jql' do + expect(subject).to eq("project = PROJECT_KEY AND reporter = \"\\\"'try\\\"some'more\\\"quote'here\\\"\" order by created DESC") + end + end + + context 'with assignee_username param' do + let(:params) { { assignee_username: "\"'try\"some'more\"quote'here\"" } } + + it 'builds jql' do + expect(subject).to eq("project = PROJECT_KEY AND assignee = \"\\\"'try\\\"some'more\\\"quote'here\\\"\" order by created DESC") + end + end + context 'with sort params' do let(:params) { { sort: 'updated', sort_direction: 'ASC' } } diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 8a1c925edc4..c49aa42b147 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Projects::ForkService do expect(from_project.forks_count).to be_zero fork_project(from_project, to_user) + BatchLoader::Executor.clear_current expect(from_project.forks_count).to eq(1) end @@ -405,6 +406,7 @@ RSpec.describe Projects::ForkService do expect(fork_from_project.forks_count).to be_zero subject.execute(fork_to_project) + BatchLoader::Executor.clear_current expect(fork_from_project.forks_count).to eq(1) end diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 9430b1d1f8a..6a2c55a5e55 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -53,6 +53,7 @@ RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_cachin expect(source.forks_count).to eq(1) subject.execute + BatchLoader::Executor.clear_current expect(source.forks_count).to be_zero end @@ -146,6 +147,7 @@ RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_cachin expect(project.forks_count).to eq(2) subject.execute + BatchLoader::Executor.clear_current expect(project.forks_count).to be_zero end @@ -212,6 +214,7 @@ RSpec.describe Projects::UnlinkForkService, :use_clean_rails_memory_store_cachin expect(forked_project.forks_count).to eq(1) subject.execute + BatchLoader::Executor.clear_current expect(project.forks_count).to eq(1) expect(forked_project.forks_count).to eq(0) diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb index 98010150e65..00ce690d2e3 100644 --- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb +++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb @@ -35,7 +35,12 @@ RSpec.shared_examples 'Maintainer manages access requests' do expect_visible_access_request(entity, user) - accept_confirm { click_on 'Deny access' } + # Open modal + click_on 'Deny access request' + + expect(page).not_to have_field "Also unassign this user from related issues and merge requests" + + click_on 'Deny access request' expect_no_visible_access_request(entity, user) expect(page).not_to have_content user.name diff --git a/spec/tooling/lib/tooling/test_file_finder_spec.rb b/spec/tooling/lib/tooling/test_file_finder_spec.rb index 3025c21d858..64b55b9b1d6 100644 --- a/spec/tooling/lib/tooling/test_file_finder_spec.rb +++ b/spec/tooling/lib/tooling/test_file_finder_spec.rb @@ -120,6 +120,22 @@ RSpec.describe Tooling::TestFileFinder do end end + context 'when given a haml view' do + let(:file) { 'app/views/admin/users/_user.html.haml' } + + it 'returns the matching view spec' do + expect(subject.test_files).to contain_exactly('spec/views/admin/users/_user.html.haml_spec.rb') + end + end + + context 'when given a haml view in ee/' do + let(:file) { 'ee/app/views/admin/users/_user.html.haml' } + + it 'returns the matching view spec' do + expect(subject.test_files).to contain_exactly('ee/spec/views/admin/users/_user.html.haml_spec.rb') + end + end + context 'when given a migration file' do let(:file) { 'db/migrate/20191023152913_add_default_and_free_plans.rb' } diff --git a/tooling/lib/tooling/test_file_finder.rb b/tooling/lib/tooling/test_file_finder.rb index 36ace67caa3..cf5de190c4a 100644 --- a/tooling/lib/tooling/test_file_finder.rb +++ b/tooling/lib/tooling/test_file_finder.rb @@ -72,9 +72,9 @@ module Tooling ImpactedTestFile.new do |impact| impact.associate(%r{app/(.+)\.rb$}) { |match| "spec/#{match[1]}_spec.rb" } impact.associate(%r{(tooling/)?lib/(.+)\.rb$}) { |match| "spec/#{match[1]}lib/#{match[2]}_spec.rb" } - impact.associate(%r{config/initializers/(.+).rb$}) { |match| "spec/initializers/#{match[1]}_spec.rb" } + impact.associate(%r{config/initializers/(.+)\.rb$}) { |match| "spec/initializers/#{match[1]}_spec.rb" } impact.associate('db/structure.sql') { 'spec/db/schema_spec.rb' } - impact.associate(%r{db/(?:post_)?migrate/([0-9]+)_(.+).rb$}) do |match| + impact.associate(%r{db/(?:post_)?migrate/([0-9]+)_(.+)\.rb$}) do |match| [ "spec/migrations/#{match[2]}_spec.rb", "spec/migrations/#{match[1]}_#{match[2]}_spec.rb" @@ -84,8 +84,9 @@ module Tooling end def either_impact - ImpactedTestFile.new(prefix: %r{^(#{EE_PREFIX})?}) do |impact| - impact.associate(%r{spec/(.+)_spec.rb$}) { |match| match[0] } + ImpactedTestFile.new(prefix: %r{^(?<prefix>#{EE_PREFIX})?}) do |impact| + impact.associate(%r{app/views/(?<view>.+)\.haml$}) { |match| "#{match[:prefix]}spec/views/#{match[:view]}.haml_spec.rb" } + impact.associate(%r{spec/(.+)_spec\.rb$}) { |match| match[0] } impact.associate(%r{spec/factories/.+\.rb$}) { 'spec/factories_spec.rb' } end end |