diff --git a/app/assets/javascripts/clusters/components/fluentd_output_settings.vue b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue index 1884b501a20..20f6210aba8 100644 --- a/app/assets/javascripts/clusters/components/fluentd_output_settings.vue +++ b/app/assets/javascripts/clusters/components/fluentd_output_settings.vue @@ -215,10 +215,10 @@ export default {
- {{ s__('ClusterIntegration|Send ModSecurity Logs') }} + {{ s__('ClusterIntegration|Send Web Application Firewall Logs') }} - {{ s__('ClusterIntegration|Send Cilium Logs') }} + {{ s__('ClusterIntegration|Send Container Network Policies Logs') }}
diff --git a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue index 24c70b99efe..54f5468bdd0 100644 --- a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue +++ b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue @@ -13,12 +13,12 @@ import { GlIcon, } from '@gitlab/ui'; import eventHub from '~/clusters/event_hub'; -import modSecurityLogo from 'images/cluster_app_logos/modsecurity.png'; +import modSecurityLogo from 'images/cluster_app_logos/gitlab.png'; const { UPDATING, UNINSTALLING, INSTALLING, INSTALLED, UPDATED } = APPLICATION_STATUS; export default { - title: 'ModSecurity Web Application Firewall', + title: __('Web Application Firewall'), modsecurityUrl: 'https://modsecurity.org/about.html', components: { GlAlert, diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index a7b7142af92..f1af8be590a 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -1,9 +1,13 @@ @@ -215,6 +266,16 @@ export default { {{ __('Delete comment') }} +
  • + +
  • diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index ada7c0465a0..0e4dd1b9c84 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -184,6 +184,7 @@ export default { 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded', + 'updateAssignees', ]), editHandler() { this.isEditing = true; @@ -299,6 +300,9 @@ export default { getLineClasses(lineNumber) { return getLineClasses(lineNumber); }, + assigneesUpdate(assignees) { + this.updateAssignees(assignees); + }, }, }; @@ -355,6 +359,7 @@ export default { ·
    diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index b441861b5de..a5b006fc301 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -647,5 +647,9 @@ export const receiveDeleteDescriptionVersionError = ({ commit }, error) => { commit(types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR, error); }; +export const updateAssignees = ({ commit }, assignees) => { + commit(types.UPDATE_ASSIGNEES, assignees); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 5c88e152280..538774ee467 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -23,6 +23,7 @@ export const REMOVE_SUGGESTION_FROM_BATCH = 'REMOVE_SUGGESTION_FROM_BATCH'; export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH'; export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION'; export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION'; +export const UPDATE_ASSIGNEES = 'UPDATE_ASSIGNEES'; // DISCUSSION export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 38f5551695d..2aeadcb2da1 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -355,4 +355,7 @@ export default { [types.RECEIVE_DELETE_DESCRIPTION_VERSION_ERROR](state) { state.isLoadingDescriptionVersion = false; }, + [types.UPDATE_ASSIGNEES](state, assignees) { + state.noteableData.assignees = assignees; + }, }; diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 803f4e37705..03504fba1ae 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -1,10 +1,12 @@ import Vue from 'vue'; import { __ } from '~/locale'; import { GlColumnChart } from '@gitlab/ui/dist/charts'; +import CodeCoverage from '../components/code_coverage.vue'; import SeriesDataMixin from './series_data_mixin'; document.addEventListener('DOMContentLoaded', () => { const languagesContainer = document.getElementById('js-languages-chart'); + const codeCoverageContainer = document.getElementById('js-code-coverage-chart'); const monthContainer = document.getElementById('js-month-chart'); const weekdayContainer = document.getElementById('js-weekday-chart'); const hourContainer = document.getElementById('js-hour-chart'); @@ -57,6 +59,18 @@ document.addEventListener('DOMContentLoaded', () => { }, }); + // eslint-disable-next-line no-new + new Vue({ + el: codeCoverageContainer, + render(h) { + return h(CodeCoverage, { + props: { + graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint, + }, + }); + }, + }); + // eslint-disable-next-line no-new new Vue({ el: monthContainer, diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue new file mode 100644 index 00000000000..af8fb032c22 --- /dev/null +++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue @@ -0,0 +1,177 @@ + + + diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 8c2fa91e515..849ca4a79f8 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -402,7 +402,6 @@ img.emoji { .prepend-top-default { margin-top: $gl-padding !important; } .prepend-top-16 { margin-top: 16px; } .prepend-top-20 { margin-top: 20px; } -.prepend-top-32 { margin-top: 32px; } .prepend-left-5 { margin-left: 5px; } .prepend-left-10 { margin-left: 10px; } .prepend-left-15 { margin-left: 15px; } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 80218d25a1a..1536c5c3022 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -149,17 +149,17 @@ $orange-800: #a35200 !default; $orange-900: #853c00 !default; $orange-950: #592800 !default; -$red-50: #fef6f5 !default; -$red-100: #fbe5e1 !default; -$red-200: #f2b4a9 !default; -$red-300: #ea8271 !default; -$red-400: #e05842 !default; -$red-500: #db3b21 !default; -$red-600: #c0341d !default; -$red-700: #a62d19 !default; -$red-800: #8b2615 !default; -$red-900: #711e11 !default; -$red-950: #4b140b !default; +$red-50: #fcf1ef !default; +$red-100: #fdd4cd !default; +$red-200: #fcb5aa !default; +$red-300: #f57f6c !default; +$red-400: #ec5941 !default; +$red-500: #dd2b0e !default; +$red-600: #c91c00 !default; +$red-700: #ae1800 !default; +$red-800: #8d1300 !default; +$red-900: #660e00 !default; +$red-950: #4d0a00 !default; $gray-10: #fafafa !default; $gray-50: #f0f0f0 !default; diff --git a/app/services/alert_management/alerts/update_service.rb b/app/services/alert_management/alerts/update_service.rb index dc2fb01f8b9..ffabbb37289 100644 --- a/app/services/alert_management/alerts/update_service.rb +++ b/app/services/alert_management/alerts/update_service.rb @@ -35,7 +35,11 @@ module AlertManagement attr_reader :alert, :current_user, :params def allowed? - current_user.can?(:update_alert_management_alert, alert) + current_user&.can?(:update_alert_management_alert, alert) + end + + def assignee_todo_allowed? + assignee&.can?(:read_alert_management_alert, alert) end def todo_service @@ -80,9 +84,10 @@ module AlertManagement end def assign_todo - return unless assignee + # Remove check in follow-up issue https://gitlab.com/gitlab-org/gitlab/-/issues/222672 + return unless assignee_todo_allowed? - todo_service.assign_alert(alert, assignee) + todo_service.assign_alert(alert, current_user) end def add_assignee_system_note(old_assignees) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 59fb25bf80e..6b1455acd08 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -22,7 +22,7 @@ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons - if can?(current_user, :push_code, @project) - .empty-wrapper.prepend-top-32 + .empty-wrapper.gl-mt-7 %h3#repo-command-line-instructions.page-title-empty = _('Command line instructions') %p diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml index 7257dacf680..24d92e947bc 100644 --- a/app/views/projects/graphs/charts.html.haml +++ b/app/views/projects/graphs/charts.html.haml @@ -28,7 +28,7 @@ %a.btn.btn-sm{ href: "#{download_path}?#{@daily_coverage_options[:base_params].to_query}" } %small = _("Download raw data (.csv)") - #js-code-coverage-chart{ data: { daily_coverage_options: @daily_coverage_options.to_json.html_safe } } + #js-code-coverage-chart{ data: { graph_endpoint: "#{@daily_coverage_options[:graph_api_path]}?#{@daily_coverage_options[:base_params].to_query}" } } .repo-charts .sub-header-block.border-top diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml index 4d392141937..e7b924c65bf 100644 --- a/app/views/projects/hook_logs/_index.html.haml +++ b/app/views/projects/hook_logs/_index.html.haml @@ -1,4 +1,4 @@ -.row.prepend-top-32.append-bottom-default +.row.gl-mt-7.append-bottom-default .col-lg-3 %h4.gl-mt-0 Recent Deliveries diff --git a/changelogs/unreleased/191455-add-a-button-to-assign-users-who-have-commented-on-an-issue.yml b/changelogs/unreleased/191455-add-a-button-to-assign-users-who-have-commented-on-an-issue.yml new file mode 100644 index 00000000000..370d8f8ceb3 --- /dev/null +++ b/changelogs/unreleased/191455-add-a-button-to-assign-users-who-have-commented-on-an-issue.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Add a button to assign users who have commented on an issue +merge_request: 23883 +author: +type: added diff --git a/changelogs/unreleased/213824-update-red-variables-in-gitlab-scss-to-match-gitlab-ui.yml b/changelogs/unreleased/213824-update-red-variables-in-gitlab-scss-to-match-gitlab-ui.yml new file mode 100644 index 00000000000..7c446d40128 --- /dev/null +++ b/changelogs/unreleased/213824-update-red-variables-in-gitlab-scss-to-match-gitlab-ui.yml @@ -0,0 +1,5 @@ +--- +title: Update red hex values to match GitLab UI +merge_request: 34544 +author: +type: other diff --git a/changelogs/unreleased/215194-add-section-to-index_approval_rule_name_for_code_owners_rule_type.yml b/changelogs/unreleased/215194-add-section-to-index_approval_rule_name_for_code_owners_rule_type.yml new file mode 100644 index 00000000000..4f5103d2305 --- /dev/null +++ b/changelogs/unreleased/215194-add-section-to-index_approval_rule_name_for_code_owners_rule_type.yml @@ -0,0 +1,5 @@ +--- +title: Add :section to approval_merge_request_rule unique index +merge_request: 33121 +author: +type: other diff --git a/changelogs/unreleased/221174-graphql-pagination-bug.yml b/changelogs/unreleased/221174-graphql-pagination-bug.yml new file mode 100644 index 00000000000..70765ae708b --- /dev/null +++ b/changelogs/unreleased/221174-graphql-pagination-bug.yml @@ -0,0 +1,5 @@ +--- +title: GraphQL - properly handle pagination of millisecond-precision timestamps +merge_request: 34352 +author: +type: fixed diff --git a/changelogs/unreleased/33743-graph-code-coverage-changes-over-time-for-a-project.yml b/changelogs/unreleased/33743-graph-code-coverage-changes-over-time-for-a-project.yml new file mode 100644 index 00000000000..d2ee2c50d00 --- /dev/null +++ b/changelogs/unreleased/33743-graph-code-coverage-changes-over-time-for-a-project.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Graph code coverage changes over time for a project +merge_request: 26174 +author: +type: added diff --git a/changelogs/unreleased/change_from_vendor_specific_to_gitlab.yml b/changelogs/unreleased/change_from_vendor_specific_to_gitlab.yml new file mode 100644 index 00000000000..f32424612b9 --- /dev/null +++ b/changelogs/unreleased/change_from_vendor_specific_to_gitlab.yml @@ -0,0 +1,5 @@ +--- +title: Change from vendor specific to Gitlab +merge_request: 34576 +author: +type: changed diff --git a/changelogs/unreleased/jcunha-bump-deploy-image-to-0-17-0.yml b/changelogs/unreleased/jcunha-bump-deploy-image-to-0-17-0.yml new file mode 100644 index 00000000000..21c60bc9871 --- /dev/null +++ b/changelogs/unreleased/jcunha-bump-deploy-image-to-0-17-0.yml @@ -0,0 +1,6 @@ +--- +title: Updated Auto DevOps with a fix to delete PostgreSQL PVC on environment cleanup, + a fix for multiline K8S_SECRET variables, updated Helm to 2.16.7 and glibc to 2.31 +merge_request: 34399 +author: verenion +type: fixed diff --git a/db/migrate/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type.rb b/db/migrate/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type.rb new file mode 100644 index 00000000000..7e31c9880cd --- /dev/null +++ b/db/migrate/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +class UpdateIndexApprovalRuleNameForCodeOwnersRuleType < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + LEGACY_INDEX_NAME_RULE_TYPE = "index_approval_rule_name_for_code_owners_rule_type" + LEGACY_INDEX_NAME_CODE_OWNERS = "approval_rule_name_index_for_code_owners" + + SECTIONAL_INDEX_NAME = "index_approval_rule_name_for_sectional_code_owners_rule_type" + + CODE_OWNER_RULE_TYPE = 2 + + def up + unless index_exists_by_name?(:approval_merge_request_rules, SECTIONAL_INDEX_NAME) + # Ensure only 1 code_owner rule with the same name and section per merge_request + # + add_concurrent_index( + :approval_merge_request_rules, + [:merge_request_id, :name, :section], + unique: true, + where: "rule_type = #{CODE_OWNER_RULE_TYPE}", + name: SECTIONAL_INDEX_NAME + ) + end + + remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_RULE_TYPE + remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_CODE_OWNERS + + add_concurrent_index( + :approval_merge_request_rules, + [:merge_request_id, :name], + unique: true, + where: "rule_type = #{CODE_OWNER_RULE_TYPE} AND section IS NULL", + name: LEGACY_INDEX_NAME_RULE_TYPE + ) + + add_concurrent_index( + :approval_merge_request_rules, + [:merge_request_id, :code_owner, :name], + unique: true, + where: "code_owner = true AND section IS NULL", + name: LEGACY_INDEX_NAME_CODE_OWNERS + ) + end + + def down + # In a rollback situation, we can't guarantee that there will not be + # records that were allowed under the more specific SECTIONAL_INDEX_NAME + # index but would cause uniqueness violations under both the + # LEGACY_INDEX_NAME_RULE_TYPE and LEGACY_INDEX_NAME_CODE_OWNERS indices. + # Therefore, we need to first find all the MergeRequests with + # ApprovalMergeRequestRules that would violate these "new" indices and + # delete those approval rules, then create the new index, then finally + # recreate the approval rules for those merge requests. + # + + # First, find all MergeRequests with ApprovalMergeRequestRules that will + # violate the new index. + # + merge_request_ids = ApprovalMergeRequestRule + .select(:merge_request_id) + .where(rule_type: CODE_OWNER_RULE_TYPE) + .group(:merge_request_id, :rule_type, :name) + .includes(:merge_request) + .having("count(*) > 1") + .collect(&:merge_request_id) + + # Delete ALL their code_owner approval rules + # + merge_request_ids.each_slice(10) do |ids| + ApprovalMergeRequestRule.where(merge_request_id: ids).code_owner.delete_all + end + + # Remove legacy partial indices that only apply to `section IS NULL` records + # + remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_RULE_TYPE + remove_concurrent_index_by_name :approval_merge_request_rules, LEGACY_INDEX_NAME_CODE_OWNERS + + # Reconstruct original "legacy" indices + # + add_concurrent_index( + :approval_merge_request_rules, + [:merge_request_id, :name], + unique: true, + where: "rule_type = #{CODE_OWNER_RULE_TYPE}", + name: LEGACY_INDEX_NAME_RULE_TYPE + ) + + add_concurrent_index( + :approval_merge_request_rules, + [:merge_request_id, :code_owner, :name], + unique: true, + where: "code_owner = true", + name: LEGACY_INDEX_NAME_CODE_OWNERS + ) + + # MergeRequest::SyncCodeOwnerApprovalRules recreates the code_owner rules + # from scratch, adding them to the index. Duplicates will be rejected. + # + merge_request_ids.each_slice(10) do |ids| + MergeRequest.where(id: ids).each do |merge_request| + MergeRequests::SyncCodeOwnerApprovalRules.new(merge_request).execute + end + end + + remove_concurrent_index_by_name :approval_merge_request_rules, SECTIONAL_INDEX_NAME + end +end diff --git a/db/migrate/20200615232735_add_index_to_composer_metadata.rb b/db/migrate/20200615232735_add_index_to_composer_metadata.rb new file mode 100644 index 00000000000..72a490c55d8 --- /dev/null +++ b/db/migrate/20200615232735_add_index_to_composer_metadata.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToComposerMetadata < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(:packages_composer_metadata, [:package_id, :target_sha], unique: true) + end + + def down + remove_concurrent_index(:packages_composer_metadata, [:package_id, :target_sha]) + end +end diff --git a/db/structure.sql b/db/structure.sql index 053f6c6dd8d..990ed37bc8e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9156,7 +9156,7 @@ CREATE UNIQUE INDEX any_approver_merge_request_rule_type_unique_index ON public. CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approval_project_rules USING btree (project_id) WHERE (rule_type = 3); -CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE (code_owner = true); +CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL)); CREATE INDEX ci_builds_gitlab_monitor_metrics ON public.ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text); @@ -9334,7 +9334,9 @@ CREATE UNIQUE INDEX index_approval_project_rules_users_1 ON public.approval_proj CREATE INDEX index_approval_project_rules_users_2 ON public.approval_project_rules_users USING btree (user_id); -CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id, name) WHERE (rule_type = 2); +CREATE UNIQUE INDEX index_approval_rule_name_for_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id, name) WHERE ((rule_type = 2) AND (section IS NULL)); + +CREATE UNIQUE INDEX index_approval_rule_name_for_sectional_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id, name, section) WHERE (rule_type = 2); CREATE INDEX index_approval_rules_code_owners_rule_type ON public.approval_merge_request_rules USING btree (merge_request_id) WHERE (rule_type = 2); @@ -10430,6 +10432,8 @@ CREATE UNIQUE INDEX index_packages_build_infos_on_package_id ON public.packages_ CREATE INDEX index_packages_build_infos_on_pipeline_id ON public.packages_build_infos USING btree (pipeline_id); +CREATE UNIQUE INDEX index_packages_composer_metadata_on_package_id_and_target_sha ON public.packages_composer_metadata USING btree (package_id, target_sha); + CREATE UNIQUE INDEX index_packages_conan_file_metadata_on_package_file_id ON public.packages_conan_file_metadata USING btree (package_file_id); CREATE UNIQUE INDEX index_packages_conan_metadata_on_package_id_username_channel ON public.packages_conan_metadata USING btree (package_id, package_username, package_channel); @@ -13959,6 +13963,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200526153844 20200526164946 20200526164947 +20200526231421 20200527092027 20200527094322 20200527095401 @@ -13995,5 +14000,6 @@ COPY "schema_migrations" (version) FROM STDIN; 20200615083635 20200615121217 20200615123055 +20200615232735 \. diff --git a/doc/ci/pipelines/img/code_coverage_graph_v13_1.png b/doc/ci/pipelines/img/code_coverage_graph_v13_1.png new file mode 100644 index 00000000000..da92a1b4a86 Binary files /dev/null and b/doc/ci/pipelines/img/code_coverage_graph_v13_1.png differ diff --git a/doc/ci/pipelines/settings.md b/doc/ci/pipelines/settings.md index b7120cb5abc..2c9d4ccf2c4 100644 --- a/doc/ci/pipelines/settings.md +++ b/doc/ci/pipelines/settings.md @@ -134,15 +134,18 @@ in the jobs table. A few examples of known coverage tools for a variety of languages can be found in the pipelines settings page. -### Download test coverage history +### Code Coverage history -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) in GitLab 12.10. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/209121) the ability to download a `.csv` in GitLab 12.10. +> - [Graph introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33743) in GitLab 13.1. If you want to see the evolution of your project code coverage over time, -you can download a CSV file with this data. From your project: +you can view a graph or download a CSV file with this data. From your project: -1. Go to **{chart}** **Project Analytics > Repository**. -1. Click **Download raw data (`.csv`)** +1. Go to **{chart}** **Project Analytics > Repository** to see the historic data for each job listed in the dropdown above the graph. +1. If you want a CSV file of that data, click **Download raw data (.csv)** + +![Code coverage graph of a project over time](img/code_coverage_graph_v13_1.png) ### Removing color codes diff --git a/doc/user/analytics/repository_analytics.md b/doc/user/analytics/repository_analytics.md index 1c810e3581a..6d2de951a55 100644 --- a/doc/user/analytics/repository_analytics.md +++ b/doc/user/analytics/repository_analytics.md @@ -33,6 +33,7 @@ The data in the charts are updated soon after each commit in the default branch. Available charts: - Programming languages used in the repository +- Code coverage history (last 3 months) ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/33743) in GitLab 13.1) - Commit statistics (last month) - Commits per day of month - Commits per weekday diff --git a/doc/user/discussions/img/quickly_assign_commenter_v13_1.png b/doc/user/discussions/img/quickly_assign_commenter_v13_1.png new file mode 100644 index 00000000000..e19a3ed4f75 Binary files /dev/null and b/doc/user/discussions/img/quickly_assign_commenter_v13_1.png differ diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index e8b9564cd03..5ee11c553af 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -532,3 +532,15 @@ to the original comment, so a note about when it was last edited will appear und This feature only exists for Issues, Merge requests, and Epics. Commits, Snippets and Merge request diff threads are not supported yet. + +## Assign an issue to the commenting user + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/191455) in GitLab 13.1. + +You can assign an issue to a user who made a comment. + +In the comment, click the **More Actions** menu and click **Assign to commenting user**. + +Click the button again to unassign the commenter. + +![Assign to commenting user](img/quickly_assign_commenter_v13_1.png) diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml index 21aace9f67a..bab4fae67f0 100644 --- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .dast-auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.17.0" dast_environment_deploy: extends: .dast-auto-deploy diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index 381b116dacb..97b5f3fd7f5 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ .auto-deploy: - image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.16.1" + image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.17.0" include: - template: Jobs/Deploy/ECS.gitlab-ci.yml diff --git a/lib/gitlab/graphql/pagination/keyset/connection.rb b/lib/gitlab/graphql/pagination/keyset/connection.rb index 508ff5bbb57..17cd22d38ad 100644 --- a/lib/gitlab/graphql/pagination/keyset/connection.rb +++ b/lib/gitlab/graphql/pagination/keyset/connection.rb @@ -194,7 +194,12 @@ module Gitlab order_list.each do |field| field_name = field.attribute_name - ordering[field_name] = node[field_name].to_s + field_value = node[field_name] + ordering[field_name] = if field_value.is_a?(Time) + field_value.strftime('%Y-%m-%d %H:%M:%S.%N %Z') + else + field_value.to_s + end end encode(ordering.to_json) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0a3d76bbb0e..acf8bf59211 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2970,6 +2970,9 @@ msgstr "" msgid "Assign to" msgstr "" +msgid "Assign to commenting user" +msgstr "" + msgid "Assign yourself to these issues" msgstr "" @@ -3415,6 +3418,9 @@ msgstr "" msgid "Beta" msgstr "" +msgid "Bi-weekly code coverage" +msgstr "" + msgid "Billing" msgstr "" @@ -5362,10 +5368,10 @@ msgstr "" msgid "ClusterIntegration|Select zone to choose machine type" msgstr "" -msgid "ClusterIntegration|Send Cilium Logs" +msgid "ClusterIntegration|Send Container Network Policies Logs" msgstr "" -msgid "ClusterIntegration|Send ModSecurity Logs" +msgid "ClusterIntegration|Send Web Application Firewall Logs" msgstr "" msgid "ClusterIntegration|Service Token" @@ -5575,6 +5581,15 @@ msgstr "" msgid "Code" msgstr "" +msgid "Code Coverage: %{coveragePercentage}%{percentSymbol}" +msgstr "" + +msgid "Code Coverage| Empty code coverage data" +msgstr "" + +msgid "Code Coverage|Couldn't fetch the code coverage data" +msgstr "" + msgid "Code Owners" msgstr "" @@ -12512,6 +12527,9 @@ msgstr "" msgid "It seems like the Dependency Scanning job ran successfully, but no dependencies have been detected in your project." msgstr "" +msgid "It seems that there is currently no available data for code coverage" +msgstr "" + msgid "It's you" msgstr "" @@ -20950,6 +20968,9 @@ msgstr "" msgid "Something went wrong while updating a requirement." msgstr "" +msgid "Something went wrong while updating assignees" +msgstr "" + msgid "Something went wrong while updating your list settings" msgstr "" @@ -24059,6 +24080,9 @@ msgstr "" msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}" msgstr "" +msgid "Unassign from commenting user" +msgstr "" + msgid "Unblock" msgstr "" @@ -25317,6 +25341,9 @@ msgstr "" msgid "We've found no vulnerabilities" msgstr "" +msgid "Web Application Firewall" +msgstr "" + msgid "Web IDE" msgstr "" @@ -26707,6 +26734,9 @@ msgstr "" msgid "customize" msgstr "" +msgid "data" +msgstr "" + msgid "date must not be after 9999-12-31" msgstr "" diff --git a/spec/frontend/clusters/components/fluentd_output_settings_spec.js b/spec/frontend/clusters/components/fluentd_output_settings_spec.js index 5e27cc49049..f03f2535947 100644 --- a/spec/frontend/clusters/components/fluentd_output_settings_spec.js +++ b/spec/frontend/clusters/components/fluentd_output_settings_spec.js @@ -70,12 +70,12 @@ describe('FluentdOutputSettings', () => { }); describe.each` - desc | changeFn | key | value - ${'when protocol dropdown is triggered'} | ${() => changeProtocol(1)} | ${'protocol'} | ${'udp'} - ${'when host is changed'} | ${() => changeHost('test-host')} | ${'host'} | ${'test-host'} - ${'when port is changed'} | ${() => changePort(123)} | ${'port'} | ${123} - ${'when wafLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send ModSecurity Logs'))} | ${'wafLogEnabled'} | ${!defaultSettings.wafLogEnabled} - ${'when ciliumLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Cilium Logs'))} | ${'ciliumLogEnabled'} | ${!defaultSettings.ciliumLogEnabled} + desc | changeFn | key | value + ${'when protocol dropdown is triggered'} | ${() => changeProtocol(1)} | ${'protocol'} | ${'udp'} + ${'when host is changed'} | ${() => changeHost('test-host')} | ${'host'} | ${'test-host'} + ${'when port is changed'} | ${() => changePort(123)} | ${'port'} | ${123} + ${'when wafLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Web Application Firewall Logs'))} | ${'wafLogEnabled'} | ${!defaultSettings.wafLogEnabled} + ${'when ciliumLogEnabled changes'} | ${() => changeCheckbox(findCheckbox('Send Container Network Policies Logs'))} | ${'ciliumLogEnabled'} | ${!defaultSettings.ciliumLogEnabled} `('$desc', ({ changeFn, key, value }) => { beforeEach(() => { changeFn(); diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js index 3aad3ac1911..220ac22d8eb 100644 --- a/spec/frontend/notes/components/note_actions_spec.js +++ b/spec/frontend/notes/components/note_actions_spec.js @@ -4,26 +4,33 @@ import { TEST_HOST } from 'spec/test_constants'; import createStore from '~/notes/stores'; import noteActions from '~/notes/components/note_actions.vue'; import { userDataMock } from '../mock_data'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; describe('noteActions', () => { let wrapper; let store; let props; + let actions; + let axiosMock; - const shallowMountNoteActions = propsData => { + const shallowMountNoteActions = (propsData, computed) => { const localVue = createLocalVue(); return shallowMount(localVue.extend(noteActions), { store, propsData, localVue, + computed, }); }; beforeEach(() => { store = createStore(); + props = { accessLevel: 'Maintainer', - authorId: 26, + authorId: 1, + author: userDataMock, canDelete: true, canEdit: true, canAwardEmoji: true, @@ -33,10 +40,17 @@ describe('noteActions', () => { reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`, showReply: false, }; + + actions = { + updateAssignees: jest.fn(), + }; + + axiosMock = new AxiosMockAdapter(axios); }); afterEach(() => { wrapper.destroy(); + axiosMock.restore(); }); describe('user is logged in', () => { @@ -76,6 +90,14 @@ describe('noteActions', () => { it('should not show copy link action when `noteUrl` prop is empty', done => { wrapper.setProps({ ...props, + author: { + avatar_url: 'mock_path', + id: 26, + name: 'Example Maintainer', + path: '/ExampleMaintainer', + state: 'active', + username: 'ExampleMaintainer', + }, noteUrl: '', }); @@ -104,6 +126,25 @@ describe('noteActions', () => { }) .catch(done.fail); }); + + it('should be possible to assign or unassign the comment author', () => { + wrapper = shallowMountNoteActions(props, { + targetType: () => 'issue', + }); + + const assignUserButton = wrapper.find('[data-testid="assign-user"]'); + expect(assignUserButton.exists()).toBe(true); + + assignUserButton.trigger('click'); + axiosMock.onPut(`${TEST_HOST}/api/v4/projects/group/project/issues/1`).reply(() => { + expect(actions.updateAssignees).toHaveBeenCalled(); + }); + }); + + it('should not be possible to assign or unassign the comment author in a merge request', () => { + const assignUserButton = wrapper.find('[data-testid="assign-user"]'); + expect(assignUserButton.exists()).toBe(false); + }); }); }); diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js index 2fef0884516..ef87cb3bee7 100644 --- a/spec/frontend/notes/stores/actions_spec.js +++ b/spec/frontend/notes/stores/actions_spec.js @@ -1141,4 +1141,17 @@ describe('Actions Notes Store', () => { }); }); }); + + describe('updateAssignees', () => { + it('update the assignees state', done => { + testAction( + actions.updateAssignees, + [userDataMock.id], + { state: noteableDataMock }, + [{ type: mutationTypes.UPDATE_ASSIGNEES, payload: [userDataMock.id] }], + [], + done, + ); + }); + }); }); diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js index f7edd66bd74..75ef007b78d 100644 --- a/spec/frontend/notes/stores/mutation_spec.js +++ b/spec/frontend/notes/stores/mutation_spec.js @@ -805,4 +805,16 @@ describe('Notes Store mutations', () => { expect(state.batchSuggestionsInfo.length).toEqual(0); }); }); + + describe('UPDATE_ASSIGNEES', () => { + it('should update assignees', () => { + const state = { + noteableData: noteableDataMock, + }; + + mutations.UPDATE_ASSIGNEES(state, [userDataMock.id]); + + expect(state.noteableData.assignees).toEqual([userDataMock.id]); + }); + }); }); diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap new file mode 100644 index 00000000000..94089ea922b --- /dev/null +++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Code Coverage when fetching data is successful matches the snapshot 1`] = ` +
    +
    + + + + + + +
    + + + + + rspec + + +
    +
    + +
    + + + + + cypress + + +
    +
    + +
    + + + + + karma + + +
    +
    +
    +
    + + +
    +`; diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js new file mode 100644 index 00000000000..4990985b076 --- /dev/null +++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js @@ -0,0 +1,164 @@ +import MockAdapter from 'axios-mock-adapter'; +import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { GlAreaChart } from '@gitlab/ui/dist/charts'; + +import axios from '~/lib/utils/axios_utils'; +import CodeCoverage from '~/pages/projects/graphs/components/code_coverage.vue'; +import codeCoverageMockData from './mock_data'; +import waitForPromises from 'helpers/wait_for_promises'; +import httpStatusCodes from '~/lib/utils/http_status'; + +describe('Code Coverage', () => { + let wrapper; + let mockAxios; + + const graphEndpoint = '/graph'; + + const findAlert = () => wrapper.find(GlAlert); + const findAreaChart = () => wrapper.find(GlAreaChart); + const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findFirstDropdownItem = () => findAllDropdownItems().at(0); + const findSecondDropdownItem = () => findAllDropdownItems().at(1); + + const createComponent = () => { + wrapper = shallowMount(CodeCoverage, { + propsData: { + graphEndpoint, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when fetching data is successful', () => { + beforeEach(() => { + mockAxios = new MockAdapter(axios); + mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData); + + createComponent(); + + return waitForPromises(); + }); + + afterEach(() => { + mockAxios.restore(); + }); + + it('renders the area chart', () => { + expect(findAreaChart().exists()).toBe(true); + }); + + it('matches the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('shows no error messages', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('when fetching data fails', () => { + beforeEach(() => { + mockAxios = new MockAdapter(axios); + mockAxios.onGet().replyOnce(httpStatusCodes.BAD_REQUEST); + + createComponent(); + + return waitForPromises(); + }); + + afterEach(() => { + mockAxios.restore(); + }); + + it('renders an error message', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().attributes().variant).toBe('danger'); + }); + + it('still renders an empty graph', () => { + expect(findAreaChart().exists()).toBe(true); + }); + }); + + describe('when fetching data succeed but returns an empty state', () => { + beforeEach(() => { + mockAxios = new MockAdapter(axios); + mockAxios.onGet().replyOnce(httpStatusCodes.OK, []); + + createComponent(); + + return waitForPromises(); + }); + + afterEach(() => { + mockAxios.restore(); + }); + + it('renders an information message', () => { + expect(findAlert().exists()).toBe(true); + expect(findAlert().attributes().variant).toBe('info'); + }); + + it('still renders an empty graph', () => { + expect(findAreaChart().exists()).toBe(true); + }); + }); + + describe('dropdown options', () => { + beforeEach(() => { + mockAxios = new MockAdapter(axios); + mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData); + + createComponent(); + + return waitForPromises(); + }); + + it('renders the dropdown with all custom names as options', () => { + expect(wrapper.contains(GlDropdown)).toBeDefined(); + expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length); + expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name); + }); + }); + + describe('interactions', () => { + beforeEach(() => { + mockAxios = new MockAdapter(axios); + mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData); + + createComponent(); + + return waitForPromises(); + }); + + it('updates the selected dropdown option with an icon', async () => { + findSecondDropdownItem().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect( + findFirstDropdownItem() + .find(GlIcon) + .exists(), + ).toBe(false); + expect(findSecondDropdownItem().contains(GlIcon)).toBe(true); + }); + + it('updates the graph data when selecting a different option in dropdown', async () => { + const originalSelectedData = wrapper.vm.selectedDailyCoverage; + const expectedData = codeCoverageMockData[1]; + + findSecondDropdownItem().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.selectedDailyCoverage).not.toBe(originalSelectedData); + expect(wrapper.vm.selectedDailyCoverage).toBe(expectedData); + }); + }); +}); diff --git a/spec/frontend/pages/projects/graphs/mock_data.js b/spec/frontend/pages/projects/graphs/mock_data.js new file mode 100644 index 00000000000..a15f861ee7a --- /dev/null +++ b/spec/frontend/pages/projects/graphs/mock_data.js @@ -0,0 +1,60 @@ +export default [ + { + group_name: 'rspec', + data: [ + { date: '2020-04-30', coverage: 40.0 }, + { date: '2020-05-01', coverage: 80.0 }, + { date: '2020-05-02', coverage: 99.0 }, + { date: '2020-05-10', coverage: 80.0 }, + { date: '2020-05-15', coverage: 70.0 }, + { date: '2020-05-20', coverage: 69.0 }, + ], + }, + { + group_name: 'cypress', + data: [ + { date: '2022-07-30', coverage: 1.0 }, + { date: '2022-08-01', coverage: 2.4 }, + { date: '2022-08-02', coverage: 5.0 }, + { date: '2022-08-10', coverage: 15.0 }, + { date: '2022-08-15', coverage: 30.0 }, + { date: '2022-08-20', coverage: 40.0 }, + ], + }, + { + group_name: 'karma', + data: [ + { date: '2020-05-01', coverage: 94.0 }, + { date: '2020-05-02', coverage: 94.0 }, + { date: '2020-05-03', coverage: 94.0 }, + { date: '2020-05-04', coverage: 94.0 }, + { date: '2020-05-05', coverage: 92.0 }, + { date: '2020-05-06', coverage: 91.0 }, + { date: '2020-05-07', coverage: 78.0 }, + { date: '2020-05-08', coverage: 94.0 }, + { date: '2020-05-09', coverage: 94.0 }, + { date: '2020-05-10', coverage: 94.0 }, + { date: '2020-05-11', coverage: 94.0 }, + { date: '2020-05-12', coverage: 94.0 }, + { date: '2020-05-13', coverage: 92.0 }, + { date: '2020-05-14', coverage: 91.0 }, + { date: '2020-05-15', coverage: 78.0 }, + { date: '2020-05-16', coverage: 94.0 }, + { date: '2020-05-17', coverage: 94.0 }, + { date: '2020-05-18', coverage: 93.0 }, + { date: '2020-05-19', coverage: 92.0 }, + { date: '2020-05-20', coverage: 91.0 }, + { date: '2020-05-21', coverage: 90.0 }, + { date: '2020-05-22', coverage: 91.0 }, + { date: '2020-05-23', coverage: 92.0 }, + { date: '2020-05-24', coverage: 75.0 }, + { date: '2020-05-25', coverage: 74.0 }, + { date: '2020-05-26', coverage: 74.0 }, + { date: '2020-05-27', coverage: 74.0 }, + { date: '2020-05-28', coverage: 80.0 }, + { date: '2020-05-29', coverage: 85.0 }, + { date: '2020-05-30', coverage: 92.0 }, + { date: '2020-05-31', coverage: 91.0 }, + ], + }, +]; diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb index 38a35261b97..ed728444b17 100644 --- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb +++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do let(:nodes) { Project.order(:updated_at) } it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s) + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z')) end it 'includes the :id even when not specified in the order' do @@ -45,7 +45,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do let(:nodes) { Project.order(:updated_at).order(:created_at) } it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s) + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z')) end end @@ -53,7 +53,7 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do let(:nodes) { Project.order(Arel.sql('projects.updated_at IS NULL')).order(:updated_at).order(:id) } it 'returns the encoded value of the order' do - expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.to_s) + expect(decoded_cursor(cursor)).to include('updated_at' => project.updated_at.strftime('%Y-%m-%d %H:%M:%S.%N %Z')) end end end diff --git a/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb b/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb new file mode 100644 index 00000000000..4aa912ef873 --- /dev/null +++ b/spec/migrations/20200526231421_update_index_approval_rule_name_for_code_owners_rule_type_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20200526231421_update_index_approval_rule_name_for_code_owners_rule_type.rb') + +describe UpdateIndexApprovalRuleNameForCodeOwnersRuleType do + let(:migration) { described_class.new } + + let(:approval_rules) { table(:approval_merge_request_rules) } + let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') } + + let(:project) do + table(:projects).create!( + namespace_id: namespace.id, + name: 'gitlab', + path: 'gitlab' + ) + end + + let(:merge_request) do + table(:merge_requests).create!( + target_project_id: project.id, + source_project_id: project.id, + target_branch: 'feature', + source_branch: 'master' + ) + end + + let(:index_names) do + ActiveRecord::Base.connection + .indexes(:approval_merge_request_rules) + .collect(&:name) + end + + def create_sectional_approval_rules + approval_rules.create!( + merge_request_id: merge_request.id, + name: "*.rb", + code_owner: true, + rule_type: 2, + section: "First Section" + ) + + approval_rules.create!( + merge_request_id: merge_request.id, + name: "*.rb", + code_owner: true, + rule_type: 2, + section: "Second Section" + ) + end + + def create_two_matching_nil_section_approval_rules + 2.times do + approval_rules.create!( + merge_request_id: merge_request.id, + name: "nil_section", + code_owner: true, + rule_type: 2 + ) + end + end + + before do + approval_rules.delete_all + end + + describe "#up" do + it "creates the new index and removes the 'legacy' indices" do + # Confirm that existing legacy indices prevent duplicate entries + # + expect { create_sectional_approval_rules } + .to raise_exception(ActiveRecord::RecordNotUnique) + expect { create_two_matching_nil_section_approval_rules } + .to raise_exception(ActiveRecord::RecordNotUnique) + + approval_rules.delete_all + + disable_migrations_output { migrate! } + + # After running the migration, expect `section == nil` rules to still + # be blocked by the legacy indices, but sectional rules are allowed. + # + expect { create_sectional_approval_rules } + .to change { approval_rules.count }.by(2) + expect { create_two_matching_nil_section_approval_rules } + .to raise_exception(ActiveRecord::RecordNotUnique) + + # Attempt to rerun the creation of sectional rules, and see that sectional + # rules are unique by section + # + expect { create_sectional_approval_rules } + .to raise_exception(ActiveRecord::RecordNotUnique) + + expect(index_names).to include( + described_class::SECTIONAL_INDEX_NAME, + described_class::LEGACY_INDEX_NAME_RULE_TYPE, + described_class::LEGACY_INDEX_NAME_CODE_OWNERS + ) + end + end + + describe "#down" do + it "recreates 'legacy' indices and removes duplicate code owner approval rules" do + disable_migrations_output { migrate! } + + expect { create_sectional_approval_rules } + .to change { approval_rules.count }.by(2) + expect { create_two_matching_nil_section_approval_rules } + .to raise_exception(ActiveRecord::RecordNotUnique) + + # Run the down migration. This will remove the 2 approval rules we create + # above, and call MergeRequests::SyncCodeOwnerApprovalRules to recreate + # new ones. + # + expect(MergeRequests::SyncCodeOwnerApprovalRules) + .to receive(:new).with(MergeRequest.find(merge_request.id)).once.and_call_original + + # We expect approval_rules.count to be changed by -2 as we're deleting the + # 3 rules created above, and MergeRequests::SyncCodeOwnerApprovalRules + # will not be able to create new one with an empty (or missing) + # CODEOWNERS file. + # + expect { disable_migrations_output { migration.down } } + .to change { approval_rules.count }.by(-3) + + # Test that the index does not allow us to create the same rules as the + # previous sectional index. + # + expect { create_sectional_approval_rules } + .to raise_exception(ActiveRecord::RecordNotUnique) + expect { create_two_matching_nil_section_approval_rules } + .to raise_exception(ActiveRecord::RecordNotUnique) + + expect(index_names).not_to include(described_class::SECTIONAL_INDEX_NAME) + expect(index_names).to include( + described_class::LEGACY_INDEX_NAME_RULE_TYPE, + described_class::LEGACY_INDEX_NAME_CODE_OWNERS + ) + end + end +end diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index f5c7a820abe..84be5ab0951 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -187,4 +187,62 @@ describe 'GraphQL' do end end end + + describe 'keyset pagination' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:issues) { create_list(:issue, 10, project: project, created_at: Time.now.change(usec: 200)) } + + let(:page_size) { 6 } + let(:issues_edges) { %w(data project issues edges) } + let(:end_cursor) { %w(data project issues pageInfo endCursor) } + let(:query) do + <<~GRAPHQL + query project($fullPath: ID!, $first: Int, $after: String) { + project(fullPath: $fullPath) { + issues(first: $first, after: $after) { + edges { node { iid } } + pageInfo { endCursor } + } + } + } + GRAPHQL + end + + # TODO: Switch this to use `post_graphql` + # This is not performing an actual GraphQL request because the + # variables end up being strings when passed through the `post_graphql` + # helper. + # + # https://gitlab.com/gitlab-org/gitlab/-/issues/222432 + def execute_query(after: nil) + GitlabSchema.execute( + query, + context: { current_user: nil }, + variables: { + fullPath: project.full_path, + first: page_size, + after: after + } + ) + end + + it 'paginates datetimes correctly when they have millisecond data' do + # let's make sure we're actually querying a timestamp, just in case + expect(Gitlab::Graphql::Pagination::Keyset::QueryBuilder) + .to receive(:new).with(anything, anything, hash_including('created_at'), anything).and_call_original + + first_page = execute_query + edges = first_page.dig(*issues_edges) + cursor = first_page.dig(*end_cursor) + + expect(edges.count).to eq(6) + expect(edges.last['node']['iid']).to eq(issues[4].iid.to_s) + + second_page = execute_query(after: cursor) + edges = second_page.dig(*issues_edges) + + expect(edges.count).to eq(4) + expect(edges.last['node']['iid']).to eq(issues[0].iid.to_s) + end + end end diff --git a/spec/services/alert_management/alerts/update_service_spec.rb b/spec/services/alert_management/alerts/update_service_spec.rb index 1701790c441..e185e67c5cf 100644 --- a/spec/services/alert_management/alerts/update_service_spec.rb +++ b/spec/services/alert_management/alerts/update_service_spec.rb @@ -20,6 +20,15 @@ describe AlertManagement::Alerts::UpdateService do describe '#execute' do subject(:response) { service.execute } + context 'when the current_user is nil' do + let(:current_user) { nil } + + it 'results in an error' do + expect(response).to be_error + expect(response.message).to eq('You have no permissions') + end + end + context 'when user does not have permission to update alerts' do let(:current_user) { user_without_permissions } @@ -81,6 +90,37 @@ describe AlertManagement::Alerts::UpdateService do expect { response }.to change { Todo.where(user: user_with_permissions).count }.by(1) end + context 'when current user is not the assignee' do + let(:assignee_user) { create(:user) } + let(:params) { { assignees: [assignee_user] } } + + it 'skips adding todo for assignee without permission to read alert' do + expect { response }.not_to change(Todo, :count) + end + + context 'when assignee has read permission' do + before do + project.add_developer(assignee_user) + end + + it 'adds a todo' do + response + + expect(Todo.first.author).to eq(current_user) + end + end + + context 'when current_user is nil' do + let(:current_user) { nil } + + it 'skips adding todo if current_user is nil' do + project.add_developer(assignee_user) + + expect { response }.not_to change(Todo, :count) + end + end + end + context 'with multiple users included' do let(:params) { { assignees: [user_with_permissions, user_without_permissions] } }