summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-09 15:08:59 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-09 15:08:59 +0000
commit4a14cfd1959c6a03758d0a75afe7b4277cf113ec (patch)
treecaa9aa524ee10076f94a6369227aaf566cbb6e74
parentfaeb202bd4a4099d4cff5a5717915883ac51422f (diff)
downloadgitlab-ce-4a14cfd1959c6a03758d0a75afe7b4277cf113ec.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/lib/utils/text_markdown.js34
-rw-r--r--app/assets/javascripts/pages/admin/groups/show/index.js24
-rw-r--r--app/assets/javascripts/pages/admin/projects/index.js22
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js30
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js26
-rw-r--r--app/assets/javascripts/pages/projects/releases/new/index.js7
-rw-r--r--app/assets/javascripts/releases/components/app_new.vue9
-rw-r--r--app/assets/javascripts/releases/mount_new.js20
-rw-r--r--app/assets/javascripts/releases/stores/modules/detail/state.js12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/remove_member_modal.vue77
-rw-r--r--app/assets/stylesheets/pages/editor.scss5
-rw-r--r--app/controllers/concerns/renders_member_access.rb6
-rw-r--r--app/controllers/concerns/renders_projects_list.rb13
-rw-r--r--app/controllers/dashboard/projects_controller.rb1
-rw-r--r--app/controllers/explore/projects_controller.rb1
-rw-r--r--app/controllers/projects/forks_controller.rb1
-rw-r--r--app/controllers/projects/pipelines/tests_controller.rb41
-rw-r--r--app/controllers/projects/releases_controller.rb20
-rw-r--r--app/controllers/root_controller.rb5
-rw-r--r--app/controllers/users_controller.rb1
-rw-r--r--app/graphql/types/issue_type.rb2
-rw-r--r--app/helpers/members_helper.rb6
-rw-r--r--app/helpers/releases_helper.rb27
-rw-r--r--app/models/ci/job_artifact.rb8
-rw-r--r--app/models/product_analytics_event.rb20
-rw-r--r--app/models/project.rb8
-rw-r--r--app/serializers/test_report_summary_entity.rb7
-rw-r--r--app/serializers/test_report_summary_serializer.rb5
-rw-r--r--app/serializers/test_suite_serializer.rb5
-rw-r--r--app/serializers/test_suite_summary_entity.rb7
-rw-r--r--app/services/jira/jql_builder_service.rb26
-rw-r--r--app/services/projects/batch_forks_count_service.rb23
-rw-r--r--app/services/projects/forks_count_service.rb2
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml1
-rw-r--r--app/views/groups/group_members/index.html.haml1
-rw-r--r--app/views/help/_shortcuts.html.haml4
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml1
-rw-r--r--app/views/projects/releases/new.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml8
-rw-r--r--changelogs/unreleased/218040-cablett-graphql-issue-id.yml5
-rw-r--r--changelogs/unreleased/220316-redis-n-1-in-api-v4-groups-id-projects-forks-count-key.yml5
-rw-r--r--changelogs/unreleased/36720-frontend-provide-option-to-unassign-removed-user-from-issuables.yml5
-rw-r--r--changelogs/unreleased/bw-surround-text-wth-char.yml5
-rw-r--r--changelogs/unreleased/nfriend-add-copy-branch-name-shortcut.yml5
-rw-r--r--changelogs/unreleased/ps-fix-single-file-editor-long-branch.yml5
-rw-r--r--config/routes/pipelines.rb8
-rw-r--r--db/fixtures/development/27_product_analytics_events.rb56
-rw-r--r--db/migrate/20200707094341_add_browser_performance_to_plan_limits.rb9
-rw-r--r--db/structure.sql4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json18
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/ci/pipelines/job_artifacts.md4
-rw-r--r--doc/ci/yaml/README.md4
-rw-r--r--doc/user/project/merge_requests/browser_performance_testing.md161
-rw-r--r--doc/user/project/merge_requests/img/browser_performance_testing.pngbin52100 -> 95312 bytes
-rw-r--r--doc/user/project/settings/import_export.md4
-rw-r--r--doc/user/shortcuts.md9
-rw-r--r--lib/api/entities/basic_project_details.rb3
-rw-r--r--lib/api/entities/group_detail.rb1
-rw-r--r--lib/api/projects_relation_builder.rb9
-rw-r--r--lib/gitlab/ci/config/entry/release.rb6
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb3
-rw-r--r--lib/gitlab/ci/reports/test_suite.rb14
-rw-r--r--lib/gitlab/ci/reports/test_suite_summary.rb4
-rw-r--r--lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml18
-rw-r--r--lib/gitlab/ci/templates/Verify/Browser-Performance.gitlab-ci.yml12
-rw-r--r--locale/gitlab.pot36
-rw-r--r--spec/controllers/projects/pipelines/tests_controller_spec.rb56
-rw-r--r--spec/factories/product_analytics_event.rb24
-rw-r--r--spec/features/groups/members/manage_members_spec.rb9
-rw-r--r--spec/features/projects/members/list_spec.rb9
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb9
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js34
-rw-r--r--spec/frontend/releases/components/app_new_spec.js26
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_header_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/remove_member_modal_spec.js65
-rw-r--r--spec/graphql/types/issue_type_spec.rb2
-rw-r--r--spec/helpers/releases_helper_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/entry/release_spec.rb52
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/test_suite_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/reports/test_suite_summary_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/browser_performance_testing_gitlab_ci_yaml_spec.rb85
-rw-r--r--spec/models/plan_limits_spec.rb1
-rw-r--r--spec/models/product_analytics_event_spec.rb17
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/serializers/test_report_summary_entity_spec.rb31
-rw-r--r--spec/serializers/test_suite_summary_entity_spec.rb24
-rw-r--r--spec/services/ci/retry_build_service_spec.rb4
-rw-r--r--spec/services/jira/jql_builder_service_spec.rb24
-rw-r--r--spec/services/projects/fork_service_spec.rb2
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb3
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb7
-rw-r--r--spec/tooling/lib/tooling/test_file_finder_spec.rb16
-rw-r--r--tooling/lib/tooling/test_file_finder.rb9
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
index 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
Binary files differ
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