diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-15 21:09:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-15 21:09:19 +0300 |
commit | c54e7283c976bf7f4394e3270a28e34655275275 (patch) | |
tree | 06bdc5fff4eeceba2911bfc71e2c14b1768b43fa | |
parent | fa7c331b76ed59230cc56fc51bfc9e467c8bb9b4 (diff) |
Add latest changes from gitlab-org/gitlab@master
88 files changed, 1332 insertions, 407 deletions
diff --git a/.rubocop_todo/lint/unused_block_argument.yml b/.rubocop_todo/lint/unused_block_argument.yml index 445b8924552..e127b529fbd 100644 --- a/.rubocop_todo/lint/unused_block_argument.yml +++ b/.rubocop_todo/lint/unused_block_argument.yml @@ -81,7 +81,6 @@ Lint/UnusedBlockArgument: - 'app/views/projects/tags/_tag.atom.builder' - 'app/workers/process_commit_worker.rb' - 'config/application.rb' - - 'config/initializers/0_log_deprecations.rb' - 'config/initializers/content_security_policy.rb' - 'config/initializers/doorkeeper.rb' - 'config/initializers/lograge.rb' diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml index 325ded06841..4d775d512c6 100644 --- a/.rubocop_todo/rspec/missing_feature_category.yml +++ b/.rubocop_todo/rspec/missing_feature_category.yml @@ -1029,7 +1029,6 @@ RSpec/MissingFeatureCategory: - 'ee/spec/lib/gitlab/geo/log_cursor/events/repository_updated_event_spec.rb' - 'ee/spec/lib/gitlab/geo/log_cursor/events/reset_checksum_event_spec.rb' - 'ee/spec/lib/gitlab/geo/log_cursor/lease_spec.rb' - - 'ee/spec/lib/gitlab/geo/log_cursor/logger_spec.rb' - 'ee/spec/lib/gitlab/geo/log_helpers_spec.rb' - 'ee/spec/lib/gitlab/geo/logger_spec.rb' - 'ee/spec/lib/gitlab/geo/oauth/login_state_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index d29ac054b2c..af33841b086 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -a41c0909051714c0d6b58f8000589f82a66804a5 +9b1bb2480275c22dd2157e08a2ec2f7db835657b @@ -348,7 +348,7 @@ gem 'pg_query', '~> 2.2', '>= 2.2.1' gem 'premailer-rails', '~> 1.10.3' -gem 'gitlab-labkit', '~> 0.29.0' +gem 'gitlab-labkit', '~> 0.30.1' gem 'thrift', '>= 0.16.0' # I18n diff --git a/Gemfile.checksum b/Gemfile.checksum index 41b0e8a3d8b..0dc38fdb8fe 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -203,7 +203,7 @@ {"name":"gitlab-dangerfiles","version":"3.6.7","platform":"ruby","checksum":"ebd898ec0e8ed3edea281b2f703000c502c6b412cbcadc1265ddbc31ffb0c579"}, {"name":"gitlab-experiment","version":"0.7.1","platform":"ruby","checksum":"166dddb3aa83428bcaa93c35684ed01dc4d61f321fd2ae40b020806dc54a7824"}, {"name":"gitlab-fog-azure-rm","version":"1.4.0","platform":"ruby","checksum":"af4163c32b028aa5208814a3f4765a5817d50527e6c61931f766bf18a2e0eb7e"}, -{"name":"gitlab-labkit","version":"0.29.0","platform":"ruby","checksum":"eb19ac5c11698683775ab847a3441d7af87d72fbaec38d635149fb65c5d9b427"}, +{"name":"gitlab-labkit","version":"0.30.1","platform":"ruby","checksum":"bdedbd86014c83dfd6a50d20dbc1709697bba2bb9e3666383e5f28cbd312b113"}, {"name":"gitlab-license","version":"2.2.1","platform":"ruby","checksum":"39fcf6be8b2887df8afe01b5dcbae8d08b7c5d937ff56b0fb40484a8c4f02d30"}, {"name":"gitlab-mail_room","version":"0.0.9","platform":"ruby","checksum":"6700374b5c0aa9d9ad4e711aeb677f0b7d415a6d01d3baa699efab25349d851c"}, {"name":"gitlab-markup","version":"1.8.1","platform":"ruby","checksum":"ab1f9fd016977497c2af25b76341dea670533014f406861834a0bd99f646707b"}, diff --git a/Gemfile.lock b/Gemfile.lock index a6df239f1a2..21966ce37c5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -584,7 +584,7 @@ GEM fog-json (~> 1.2.0) mime-types ms_rest_azure (~> 0.12.0) - gitlab-labkit (0.29.0) + gitlab-labkit (0.30.1) actionpack (>= 5.0.0, < 8.0.0) activesupport (>= 5.0.0, < 8.0.0) grpc (>= 1.37) @@ -1679,7 +1679,7 @@ DEPENDENCIES gitlab-dangerfiles (~> 3.6.7) gitlab-experiment (~> 0.7.1) gitlab-fog-azure-rm (~> 1.4.0) - gitlab-labkit (~> 0.29.0) + gitlab-labkit (~> 0.30.1) gitlab-license (~> 2.2.1) gitlab-mail_room (~> 0.0.9) gitlab-markup (~> 1.8.0) diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index 097f2617c1d..9d33f051ad0 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -1,17 +1,19 @@ +import { escape } from 'lodash'; import { spriteIcon } from '~/lib/utils/common_utils'; import { differenceInMilliseconds } from '~/lib/utils/datetime_utility'; -import { s__ } from '~/locale'; +import { s__, sprintf } from '~/locale'; import { unrestrictedPages } from './constants'; -// Renders math using KaTeX in any element with the -// `js-render-math` class +// Renders math using KaTeX in an element. // -// ### Example Markup -// -// <code class="js-render-math"></div> +// Typically for elements with the `js-render-math` class such as +// <code class="js-render-math"></code> // +// See app/assets/javascripts/behaviors/markdown/render_gfm.js const MAX_MATH_CHARS = 1000; +const MAX_MACRO_EXPANSIONS = 1000; +const MAX_USER_SPECIFIED_EMS = 20; const MAX_RENDER_TIME_MS = 2000; // Wait for the browser to reflow the layout. Reflowing SVG takes time. @@ -74,13 +76,23 @@ class SafeMathRenderer { const { parentNode } = el; parentNode.replaceChild(wrapperElement, el); + let message; + if (text.length > MAX_MATH_CHARS) { + message = sprintf( + s__( + 'math|This math block exceeds %{maxMathChars} characters, and may cause performance issues on this page.', + ), + { maxMathChars: MAX_MATH_CHARS }, + ); + } else { + message = s__('math|Displaying this math block may cause performance issues on this page.'); + } + const html = ` <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-math-container js-lazy-render-math-container fade show" role="alert"> ${spriteIcon('warning', 'text-warning-600 s16 gl-alert-icon')} <div class="display-flex gl-alert-content"> - <div>${s__( - 'math|Displaying this math block may cause performance issues on this page', - )}</div> + <div>${message}</div> <div class="gl-alert-actions"> <button class="js-lazy-render-math btn gl-alert-action btn-confirm btn-md gl-button">Display anyway</button> </div> @@ -117,8 +129,10 @@ class SafeMathRenderer { displayContainer.innerHTML = this.katex.renderToString(text, { displayMode: el.dataset.mathStyle === 'display', throwOnError: true, - maxSize: 20, - maxExpand: 20, + maxSize: MAX_USER_SPECIFIED_EMS, + // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111107 for + // reasoning behind this value + maxExpand: MAX_MACRO_EXPANSIONS, trust: (context) => // this config option restores the KaTeX pre-v0.11.0 // behavior of allowing certain commands and protocols @@ -128,8 +142,17 @@ class SafeMathRenderer { }); } catch (e) { // Don't show a flash for now because it would override an existing flash message - el.textContent = s__('math|There was an error rendering this math block'); - // el.style.color = '#d00'; + if (e.message.match(/Too many expansions/)) { + // this is controlled by the maxExpand parameter + el.textContent = s__('math|Too many expansions. Consider using multiple math blocks.'); + } else { + // According to https://katex.org/docs/error.html, we need to ensure that + // the error message is escaped. + el.textContent = sprintf( + s__('math|There was an error rendering this math block. %{katexMessage}'), + { katexMessage: escape(e.message) }, + ); + } el.className = 'katex-error'; } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/bold_text.vue b/app/assets/javascripts/vue_merge_request_widget/components/bold_text.vue new file mode 100644 index 00000000000..bef1d79a655 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/bold_text.vue @@ -0,0 +1,26 @@ +<script> +import { GlSprintf } from '@gitlab/ui'; + +export default { + name: 'BoldText', + components: { + GlSprintf, + }, + props: { + message: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <span> + <gl-sprintf :message="message"> + <template #bold="{ content }"> + <span class="gl-font-weight-bold" v-text="content"></span> + </template> + </gl-sprintf> + </span> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue index e5688091cc7..6d7ec607557 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue @@ -1,17 +1,23 @@ <script> import { s__ } from '~/locale'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import StateContainer from '../state_container.vue'; import { DETAILED_MERGE_STATUS } from '../../constants'; export default { i18n: { - approvalNeeded: s__('mrWidget|Merge blocked: all required approvals must be given.'), + approvalNeeded: s__( + 'mrWidget|%{boldStart}Merge blocked:%{boldEnd} all required approvals must be given.', + ), blockingMergeRequests: s__( - 'mrWidget|Merge blocked: you can only merge after the above items are resolved.', + 'mrWidget|%{boldStart}Merge blocked:%{boldEnd} you can only merge after the above items are resolved.', + ), + externalStatusChecksFailed: s__( + 'mrWidget|%{boldStart}Merge blocked:%{boldEnd} all status checks must pass.', ), - externalStatusChecksFailed: s__('mrWidget|Merge blocked: all status checks must pass.'), }, components: { + BoldText, StateContainer, }, props: { @@ -38,10 +44,8 @@ export default { <template> <state-container :mr="mr" status="failed"> - <span - class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!" - > - {{ failedText }} + <span class="gl-ml-3 gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"> + <bold-text :message="failedText" /> </span> </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue index 79e878431ed..837f8b32637 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue @@ -1,9 +1,17 @@ <script> +import { s__ } from '~/locale'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import StateContainer from '../state_container.vue'; +const message = s__( + 'mrWidget|%{boldStart}Merge unavailable:%{boldEnd} merge requests are read-only on archived projects.', +); + export default { name: 'MRWidgetArchived', + message, components: { + BoldText, StateContainer, }, props: { @@ -17,8 +25,6 @@ export default { <template> <state-container :mr="mr" status="failed"> - <span class="gl-font-weight-bold"> - {{ s__('mrWidget|Merge unavailable: merge requests are read-only on archived projects.') }} - </span> + <bold-text :message="$options.message" /> </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue index 922075516f3..670bd36d61e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue @@ -16,8 +16,6 @@ export default { </script> <template> <state-container :mr="mr" status="loading"> - <span class="gl-font-weight-bold"> - {{ s__('mrWidget|Checking if merge request can be merged…') }} - </span> + {{ s__('mrWidget|Checking if merge request can be merged…') }} </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index a5d982fe221..19701125978 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -1,5 +1,7 @@ <script> import { GlButton, GlSkeletonLoader } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import userPermissionsQuery from '../../queries/permissions.query.graphql'; import conflictsStateQuery from '../../queries/states/conflicts.query.graphql'; @@ -8,6 +10,7 @@ import StateContainer from '../state_container.vue'; export default { name: 'MRWidgetConflicts', components: { + BoldText, GlSkeletonLoader, GlButton, StateContainer, @@ -55,6 +58,17 @@ export default { ); }, }, + i18n: { + shouldBeRebased: s__( + 'mrWidget|%{boldStart}Merge blocked:%{boldEnd} fast-forward merge is not possible. To merge this request, first rebase locally.', + ), + shouldBeResolved: s__( + 'mrWidget|%{boldStart}Merge blocked:%{boldEnd} merge conflicts must be resolved.', + ), + usersWriteBranches: s__( + 'mrWidget|%{boldStart}Merge blocked:%{boldEnd} Users who can write to the source or target branches can resolve the conflicts.', + ), + }, }; </script> <template> @@ -67,21 +81,14 @@ export default { </gl-skeleton-loader> </template> <template v-if="!isLoading"> - <span v-if="state.shouldBeRebased" class="bold gl-ml-0! gl-text-body!"> - {{ - s__(`mrWidget|Merge blocked: fast-forward merge is not possible. - To merge this request, first rebase locally.`) - }} + <span v-if="state.shouldBeRebased" class="gl-ml-0! gl-text-body!"> + <bold-text :message="$options.i18n.shouldBeRebased" /> </span> <template v-else> - <span class="bold gl-ml-0! gl-text-body! gl-flex-grow-1 gl-w-full gl-md-w-auto gl-mr-2"> - {{ s__('mrWidget|Merge blocked: merge conflicts must be resolved.') }} + <span class="gl-ml-0! gl-text-body! gl-flex-grow-1 gl-w-full gl-md-w-auto gl-mr-2"> + <bold-text :message="$options.i18n.shouldBeResolved" /> <span v-if="!userPermissions.canMerge"> - {{ - s__( - `mrWidget|Users who can write to the source or target branches can resolve the conflicts.`, - ) - }} + {{ $options.i18n.usersWriteBranches }} </span> </span> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue index 8a7f15d8d1a..bfc2c282f4c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -101,12 +101,14 @@ export default { </span> </state-container> <state-container v-else :mr="mr" status="failed" :actions="actions"> - <span class="gl-font-weight-bold"> - <span v-if="mr.mergeError" class="has-error-message" data-testid="merge-error"> - {{ mergeError }} - </span> - <span v-else> {{ s__('mrWidget|Merge failed.') }} </span> - <span :class="{ 'has-custom-error': mr.mergeError }"> {{ timerText }} </span> + <span + v-if="mr.mergeError" + class="has-error-message gl-font-weight-bold" + data-testid="merge-error" + > + {{ mergeError }} </span> + <span v-else class="gl-font-weight-bold"> {{ s__('mrWidget|Merge failed.') }} </span> + <span :class="{ 'has-custom-error': mr.mergeError }"> {{ timerText }} </span> </state-container> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue index 51ac2576f75..c94718ca756 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue @@ -2,6 +2,7 @@ import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import simplePoll from '~/lib/utils/simple_poll'; import MergeRequest from '~/merge_request'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import eventHub from '../../event_hub'; import { MERGE_ACTIVE_STATUS_PHRASES, STATE_MACHINE } from '../../constants'; import StatusIcon from '../mr_widget_status_icon.vue'; @@ -12,6 +13,7 @@ const { MERGE_FAILURE } = transitions; export default { name: 'MRWidgetMerging', components: { + BoldText, StatusIcon, }, props: { @@ -83,11 +85,9 @@ export default { <template> <div class="mr-widget-body mr-state-locked media"> <status-icon status="loading" /> - <div class="media-body"> - <h4> - {{ mergeStatus.message }} - <gl-emoji :data-name="mergeStatus.emoji" /> - </h4> + <div class="media-body" data-testid="merging-state"> + <bold-text :message="mergeStatus.message" /> + <gl-emoji :data-name="mergeStatus.emoji" /> </div> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue index 5e073bf7c04..f1ddf94597b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue @@ -63,12 +63,14 @@ export default { <status-icon :show-disabled-button="true" status="failed" /> <div class="media-body space-children"> - <span class="gl-font-weight-bold js-branch-text" data-testid="widget-content"> - <gl-sprintf :message="warning"> - <template #code="{ content }"> - <code>{{ content }}</code> - </template> - </gl-sprintf> + <span class="js-branch-text" data-testid="widget-content"> + <span class="gl-font-weight-bold"> + <gl-sprintf :message="warning"> + <template #code="{ content }"> + <code>{{ content }}</code> + </template> + </gl-sprintf> + </span> {{ restore }} <gl-icon v-gl-tooltip diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue index d837551a813..536e61e57d3 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue @@ -1,9 +1,17 @@ <script> +import { s__ } from '~/locale'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import StatusIcon from '../mr_widget_status_icon.vue'; +const message = s__( + 'mrWidget|%{boldStart}Ready to be merged automatically.%{boldEnd} Ask someone with write access to this repository to merge this request.', +); + export default { name: 'MRWidgetNotAllowed', + message, components: { + BoldText, StatusIcon, }, }; @@ -13,12 +21,7 @@ export default { <div class="mr-widget-body media"> <status-icon status="success" /> <div class="media-body space-children"> - <span class="gl-font-weight-bold"> - {{ - s__(`mrWidget|Ready to be merged automatically. -Ask someone with write access to this repository to merge this request`) - }} - </span> + <bold-text :message="$options.message" /> </div> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue index 13920daca15..beb6310992f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue @@ -1,10 +1,18 @@ <script> +import { s__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import StatusIcon from '../mr_widget_status_icon.vue'; +const message = s__( + "mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. It's waiting for a manual action to continue.", +); + export default { name: 'MRWidgetPipelineBlocked', + message, components: { + BoldText, StatusIcon, }, mixins: [glFeatureFlagMixin()], @@ -14,13 +22,7 @@ export default { <div class="mr-widget-body media"> <status-icon status="failed" /> <div class="media-body space-children"> - <span class="gl-font-weight-bold"> - {{ - s__( - `mrWidget|Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.`, - ) - }} - </span> + <bold-text :message="$options.message" /> </div> </div> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index d687f0346c7..ec6c2cf34c0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -1,16 +1,24 @@ <script> import { GlButton, GlSkeletonLoader } from '@gitlab/ui'; import { createAlert } from '~/flash'; -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; import toast from '~/vue_shared/plugins/global_toast'; import simplePoll from '~/lib/utils/simple_poll'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import eventHub from '../../event_hub'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import rebaseQuery from '../../queries/states/rebase.query.graphql'; import StateContainer from '../state_container.vue'; +const i18n = { + rebaseError: s__( + 'mrWidget|%{boldStart}Merge blocked:%{boldEnd} the source branch must be rebased onto the target branch.', + ), +}; + export default { name: 'MRWidgetRebase', + i18n, apollo: { state: { query: rebaseQuery, @@ -21,6 +29,7 @@ export default { }, }, components: { + BoldText, GlSkeletonLoader, GlButton, StateContainer, @@ -69,9 +78,6 @@ export default { } return 'success'; }, - fastForwardMergeText() { - return __('Merge blocked: the source branch must be rebased onto the target branch.'); - }, showRebaseWithoutPipeline() { return ( !this.mr.onlyAllowMergeIfPipelineSucceeds || @@ -146,29 +152,29 @@ export default { <template v-if="!isLoading"> <span v-if="rebaseInProgress || isMakingRequest" - class="gl-ml-0! gl-text-body! gl-font-weight-bold" + class="gl-ml-0! gl-text-body!" data-testid="rebase-message" - >{{ __('Rebase in progress') }}</span + >{{ s__('mrWidget|Rebase in progress') }}</span > <span v-if="!rebaseInProgress && !canPushToSourceBranch" - class="gl-text-body! gl-font-weight-bold gl-ml-0!" + class="gl-text-body! gl-ml-0!" data-testid="rebase-message" - >{{ fastForwardMergeText }}</span > + <bold-text :message="$options.i18n.rebaseError" /> + </span> <div v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest" class="accept-merge-holder clearfix js-toggle-container media gl-md-display-flex gl-flex-wrap gl-flex-grow-1" > <span v-if="!rebasingError" - class="gl-font-weight-bold gl-w-100 gl-md-w-auto gl-flex-grow-1 gl-ml-0! gl-text-body! gl-md-mr-3" + class="gl-w-100 gl-md-w-auto gl-flex-grow-1 gl-ml-0! gl-text-body! gl-md-mr-3" data-testid="rebase-message" data-qa-selector="no_fast_forward_message_content" - >{{ - __('Merge blocked: the source branch must be rebased onto the target branch.') - }}</span > + <bold-text :message="$options.i18n.rebaseError" /> + </span> <span v-else class="gl-font-weight-bold danger gl-w-100 gl-md-w-auto gl-flex-grow-1 gl-md-mr-3" @@ -187,7 +193,7 @@ export default { class="gl-align-self-start" @click="rebase" > - {{ __('Rebase') }} + {{ s__('mrWidget|Rebase') }} </gl-button> <gl-button v-if="showRebaseWithoutPipeline" @@ -199,7 +205,7 @@ export default { class="gl-align-self-start gl-mr-2" @click="rebaseWithoutCi" > - {{ __('Rebase without pipeline') }} + {{ s__('mrWidget|Rebase without pipeline') }} </gl-button> </template> </state-container> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue index 853895a4296..1896851952b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue @@ -2,11 +2,13 @@ import { GlLink, GlSprintf } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import { s__ } from '~/locale'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import StatusIcon from '../mr_widget_status_icon.vue'; export default { name: 'PipelineFailed', components: { + BoldText, GlLink, GlSprintf, StatusIcon, @@ -24,7 +26,10 @@ export default { }, i18n: { failedMessage: s__( - `mrWidget|Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or %{linkStart}learn about other solutions.%{linkEnd}`, + `mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. Push a commit that fixes the failure or %{linkStart}learn about other solutions.%{linkEnd}`, + ), + blockedMessage: s__( + "mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. It's waiting for a manual action to continue.", ), }, }; @@ -34,20 +39,17 @@ export default { <div class="mr-widget-body media"> <status-icon status="failed" /> <div class="media-body space-children"> - <span class="gl-font-weight-bold"> - <span v-if="mr.isPipelineBlocked"> - {{ - s__( - `mrWidget|Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.`, - ) - }} - </span> + <span> + <bold-text v-if="mr.isPipelineBlocked" :message="$options.i18n.blockedMessage" /> <gl-sprintf v-else :message="$options.i18n.failedMessage"> <template #link="{ content }"> <gl-link :href="troubleshootingDocsPath" target="_blank"> {{ content }} </gl-link> </template> + <template #bold="{ content }"> + <span class="gl-font-weight-bold">{{ content }}</span> + </template> </gl-sprintf> </span> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue index 27919f90cc3..2aa345b420e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue @@ -1,11 +1,13 @@ <script> import { GlButton } from '@gitlab/ui'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import { I18N_SHA_MISMATCH } from '../../i18n'; import StateContainer from '../state_container.vue'; export default { name: 'ShaMismatch', components: { + BoldText, GlButton, StateContainer, }, @@ -24,10 +26,10 @@ export default { <template> <state-container :mr="mr" status="failed"> <span - class="gl-font-weight-bold gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!" + class="gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!" data-qa-selector="head_mismatch_content" > - {{ $options.i18n.I18N_SHA_MISMATCH.warningMessage }} + <bold-text :message="$options.i18n.I18N_SHA_MISMATCH.warningMessage" /> </span> <template #actions> <gl-button diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index 9f3748599dc..0fd5551979d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -1,11 +1,17 @@ <script> import { GlButton } from '@gitlab/ui'; +import { s__ } from '~/locale'; import notesEventHub from '~/notes/event_hub'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import StateContainer from '../state_container.vue'; +const message = s__('mrWidget|%{boldStart}Merge blocked:%{boldEnd} all threads must be resolved.'); + export default { name: 'UnresolvedDiscussions', + message, components: { + BoldText, GlButton, StateContainer, }, @@ -25,10 +31,8 @@ export default { <template> <state-container :mr="mr" status="failed"> - <span - class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!" - > - {{ s__('mrWidget|Merge blocked: all threads must be resolved.') }} + <span class="gl-ml-3 gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"> + <bold-text :message="$options.message" /> </span> <template #actions> <gl-button diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index 1004f2ab3ad..02d4f2499fe 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -4,6 +4,7 @@ import { produce } from 'immer'; import { createAlert } from '~/flash'; import { __, s__ } from '~/locale'; import MergeRequest from '~/merge_request'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import getStateQuery from '../../queries/get_state.query.graphql'; import draftQuery from '../../queries/states/draft.query.graphql'; @@ -12,14 +13,12 @@ import StateContainer from '../state_container.vue'; // Export for testing export const MSG_SOMETHING_WENT_WRONG = __('Something went wrong. Please try again.'); -export const MSG_MERGE_BLOCKED = __( - "Merge blocked: merge request must be marked as ready. It's still marked as draft.", -); export const MSG_MARK_READY = s__('mrWidget|Mark as ready'); export default { name: 'WorkInProgress', components: { + BoldText, GlButton, StateContainer, }, @@ -128,15 +127,19 @@ export default { }); }, }, - MSG_MERGE_BLOCKED, + i18n: { + removeDraftStatus: s__( + 'mrWidget|%{boldStart}Merge blocked:%{boldEnd} Select %{boldStart}Mark as ready%{boldEnd} to remove it from Draft status.', + ), + }, MSG_MARK_READY, }; </script> <template> <state-container :mr="mr" status="failed"> - <span class="gl-font-weight-bold gl-ml-0! gl-text-body! gl-flex-grow-1"> - {{ $options.MSG_MERGE_BLOCKED }} + <span class="gl-ml-0! gl-text-body! gl-flex-grow-1"> + <bold-text :message="$options.i18n.removeDraftStatus" /> </span> <template #actions> <gl-button diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js index 7109bed7743..85ae298fcea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/constants.js +++ b/app/assets/javascripts/vue_merge_request_widget/constants.js @@ -4,9 +4,7 @@ import { stateToComponentMap as classStateMap, stateKey } from './stores/state_m export const SUCCESS = 'success'; export const WARNING = 'warning'; -export const DANGER = 'danger'; export const INFO = 'info'; -export const CONFIRM = 'confirm'; export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds'; export const MTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds'; @@ -28,39 +26,39 @@ export const SP_ICON_NAME = 'status_notfound'; export const MERGE_ACTIVE_STATUS_PHRASES = [ { - message: s__('mrWidget|Merging! Drum roll, please…'), + message: s__('mrWidget|%{boldStart}Merging!%{boldEnd} Drum roll, please…'), emoji: 'drum', }, { - message: s__("mrWidget|Merging! We're almost there…"), + message: s__("mrWidget|%{boldStart}Merging!%{boldEnd} We're almost there…"), emoji: 'sparkles', }, { - message: s__('mrWidget|Merging! Changes will land soon…'), + message: s__('mrWidget|%{boldStart}Merging!%{boldEnd} Changes will land soon…'), emoji: 'airplane_arriving', }, { - message: s__('mrWidget|Merging! Changes are being shipped…'), + message: s__('mrWidget|%{boldStart}Merging!%{boldEnd} Changes are being shipped…'), emoji: 'ship', }, { - message: s__("mrWidget|Merging! Everything's good…"), + message: s__("mrWidget|%{boldStart}Merging!%{boldEnd} Everything's good…"), emoji: 'relieved', }, { - message: s__('mrWidget|Merging! This is going to be great…'), + message: s__('mrWidget|%{boldStart}Merging!%{boldEnd} This is going to be great…'), emoji: 'heart_eyes', }, { - message: s__('mrWidget|Merging! Lift-off in 5… 4… 3…'), + message: s__('mrWidget|%{boldStart}Merging!%{boldEnd} Lift-off in 5… 4… 3…'), emoji: 'rocket', }, { - message: s__('mrWidget|Merging! The changes are leaving the station…'), + message: s__('mrWidget|%{boldStart}Merging!%{boldEnd} The changes are leaving the station…'), emoji: 'bullettrain_front', }, { - message: s__('mrWidget|Merging! Take a deep breath and relax…'), + message: s__('mrWidget|%{boldStart}Merging!%{boldEnd} Take a deep breath and relax…'), emoji: 'sunglasses', }, ]; diff --git a/app/assets/javascripts/vue_merge_request_widget/i18n.js b/app/assets/javascripts/vue_merge_request_widget/i18n.js index 5380bcae003..5ca56074031 100644 --- a/app/assets/javascripts/vue_merge_request_widget/i18n.js +++ b/app/assets/javascripts/vue_merge_request_widget/i18n.js @@ -17,7 +17,7 @@ export const SQUASH_BEFORE_MERGE = { }; export const I18N_SHA_MISMATCH = { - warningMessage: __('Merge blocked: new changes were just added.'), + warningMessage: s__('mrWidget|%{boldStart}Merge blocked:%{boldEnd} new changes were just added.'), actionButtonLabel: __('Review changes'), }; diff --git a/app/assets/javascripts/vue_shared/components/url_sync.vue b/app/assets/javascripts/vue_shared/components/url_sync.vue index ba5bce32b40..ad81c14d9e5 100644 --- a/app/assets/javascripts/vue_shared/components/url_sync.vue +++ b/app/assets/javascripts/vue_shared/components/url_sync.vue @@ -1,7 +1,9 @@ <script> -import { historyPushState } from '~/lib/utils/common_utils'; +import { historyPushState, historyReplaceState } from '~/lib/utils/common_utils'; import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility'; +export const HISTORY_PUSH_UPDATE_METHOD = 'push'; +export const HISTORY_REPLACE_UPDATE_METHOD = 'replace'; export const URL_SET_PARAMS_STRATEGY = 'set'; export const URL_MERGE_PARAMS_STRATEGY = 'merge'; @@ -24,6 +26,13 @@ export default { default: URL_MERGE_PARAMS_STRATEGY, validator: (value) => [URL_MERGE_PARAMS_STRATEGY, URL_SET_PARAMS_STRATEGY].includes(value), }, + historyUpdateMethod: { + type: String, + required: false, + default: HISTORY_PUSH_UPDATE_METHOD, + validator: (value) => + [HISTORY_PUSH_UPDATE_METHOD, HISTORY_REPLACE_UPDATE_METHOD].includes(value), + }, }, watch: { query: { @@ -42,7 +51,12 @@ export default { this.urlParamsUpdateStrategy === URL_SET_PARAMS_STRATEGY ? setUrlParams(this.query, window.location.href, true, true, true) : mergeUrlParams(newQuery, window.location.href, { spreadArrays: true }); - historyPushState(url); + + if (this.historyUpdateMethod === HISTORY_PUSH_UPDATE_METHOD) { + historyPushState(url); + } else { + historyReplaceState(url); + } }, }, render() { diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index 35f295630f7..b003ca564f3 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -36,17 +36,20 @@ class DashboardController < Dashboard::ApplicationController def load_events @events = - if params[:filter] == "followed" - load_user_events - else + case params[:filter] + when "projects", "starred" load_project_events + when "followed" + load_user_events(current_user.followees) + else + load_user_events(current_user) end Events::RenderService.new(current_user).execute(@events) end - def load_user_events - UserRecentEventsFinder.new(current_user, current_user.followees, event_filter, params).execute + def load_user_events(user) + UserRecentEventsFinder.new(current_user, user, event_filter, params).execute end def load_project_events diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 97b6671a82a..71da9bdcbc4 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -41,8 +41,10 @@ class RootController < Dashboard::ProjectsController when 'stars' flash.keep redirect_to(starred_dashboard_projects_path) - when 'project_activity' + when 'your_activity' redirect_to(activity_dashboard_path) + when 'project_activity' + redirect_to(activity_dashboard_path(filter: 'projects')) when 'starred_project_activity' redirect_to(activity_dashboard_path(filter: 'starred')) when 'followed_user_activity' diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 88e68a52199..2442856d7fe 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -30,6 +30,7 @@ module PreferencesHelper { projects: _("Your Projects (default)"), stars: _("Starred Projects"), + your_activity: _("Your Activity"), project_activity: _("Your Projects' Activity"), starred_project_activity: _("Starred Projects' Activity"), followed_user_activity: _("Followed Users' Activity"), diff --git a/app/models/container_registry/event.rb b/app/models/container_registry/event.rb index d4075e1ff1b..c4d06be8841 100644 --- a/app/models/container_registry/event.rb +++ b/app/models/container_registry/event.rb @@ -8,6 +8,21 @@ module ContainerRegistry PUSH_ACTION = 'push' DELETE_ACTION = 'delete' EVENT_TRACKING_CATEGORY = 'container_registry:notification' + EVENT_PREFIX = "i_container_registry" + + ALLOWED_ACTOR_TYPES = %w( + personal_access_token + build + gitlab_or_ldap + ).freeze + + TRACKABLE_ACTOR_EVENTS = %w( + push_tag + delete_tag + push_repository + delete_repository + create_repository + ).freeze attr_reader :event @@ -32,6 +47,9 @@ module ContainerRegistry end ::Gitlab::Tracking.event(EVENT_TRACKING_CATEGORY, tracking_action) + + event = usage_data_event_for(tracking_action) + ::Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event, values: originator.id) if event end private @@ -81,6 +99,29 @@ module ContainerRegistry container_registry_path&.repository_project end + # counter name for unique user tracking (for MAU) + def usage_data_event_for(tracking_action) + return unless originator + return unless TRACKABLE_ACTOR_EVENTS.include?(tracking_action) + + "#{EVENT_PREFIX}_#{tracking_action}_user" + end + + def originator_type + event.dig('actor', 'user_type') + end + + def originator + return unless ALLOWED_ACTOR_TYPES.include?(originator_type) + + username = event.dig('actor', 'name') + return unless username + + strong_memoize(:originator) do + User.find_by_username(username) + end + end + def update_project_statistics return unless supported? return unless target_tag? || (action_delete? && target_digest?) diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 11f4a3f3b6f..168646bbe41 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -63,32 +63,23 @@ class ProjectFeature < ApplicationRecord validate :repository_children_level - default_value_for :builds_access_level, value: ENABLED, allows_nil: false - default_value_for :issues_access_level, value: ENABLED, allows_nil: false - default_value_for :forking_access_level, value: ENABLED, allows_nil: false - default_value_for :merge_requests_access_level, value: ENABLED, allows_nil: false - default_value_for :snippets_access_level, value: ENABLED, allows_nil: false - default_value_for :wiki_access_level, value: ENABLED, allows_nil: false - default_value_for :repository_access_level, value: ENABLED, allows_nil: false - default_value_for :analytics_access_level, value: ENABLED, allows_nil: false - default_value_for :metrics_dashboard_access_level, value: PRIVATE, allows_nil: false - default_value_for :operations_access_level, value: ENABLED, allows_nil: false - default_value_for :security_and_compliance_access_level, value: PRIVATE, allows_nil: false - default_value_for :monitor_access_level, value: ENABLED, allows_nil: false - default_value_for :infrastructure_access_level, value: ENABLED, allows_nil: false - default_value_for :feature_flags_access_level, value: ENABLED, allows_nil: false - default_value_for :environments_access_level, value: ENABLED, allows_nil: false - default_value_for :releases_access_level, value: ENABLED, allows_nil: false - - default_value_for(:pages_access_level, allows_nil: false) do |feature| - if ::Gitlab::Pages.access_control_is_forced? - PRIVATE - else - feature.project&.public? ? ENABLED : PRIVATE - end - end - - default_value_for(:package_registry_access_level) do |feature| + attribute :builds_access_level, default: ENABLED + attribute :issues_access_level, default: ENABLED + attribute :forking_access_level, default: ENABLED + attribute :merge_requests_access_level, default: ENABLED + attribute :snippets_access_level, default: ENABLED + attribute :wiki_access_level, default: ENABLED + attribute :repository_access_level, default: ENABLED + attribute :analytics_access_level, default: ENABLED + attribute :metrics_dashboard_access_level, default: PRIVATE + attribute :operations_access_level, default: ENABLED + attribute :security_and_compliance_access_level, default: PRIVATE + attribute :monitor_access_level, default: ENABLED + attribute :infrastructure_access_level, default: ENABLED + attribute :feature_flags_access_level, default: ENABLED + attribute :environments_access_level, default: ENABLED + + attribute :package_registry_access_level, default: -> do if ::Gitlab.config.packages.enabled ENABLED else @@ -96,7 +87,7 @@ class ProjectFeature < ApplicationRecord end end - default_value_for(:container_registry_access_level) do |feature| + attribute :container_registry_access_level, default: -> do if gitlab_config_features.container_registry ENABLED else @@ -104,6 +95,9 @@ class ProjectFeature < ApplicationRecord end end + after_initialize :set_pages_access_level, if: :new_record? + after_initialize :set_default_values, unless: :new_record? + # "enabled" here means "not disabled". It includes private features! scope :with_feature_enabled, ->(feature) { feature_access_level_attribute = arel_table[access_level_attribute(feature)] @@ -170,6 +164,23 @@ class ProjectFeature < ApplicationRecord private + def set_pages_access_level + self.pages_access_level ||= if ::Gitlab::Pages.access_control_is_forced? + PRIVATE + else + self.project&.public? ? ENABLED : PRIVATE + end + end + + def set_default_values + self.class.column_names.each do |column_name| + next unless has_attribute?(column_name) + next unless read_attribute(column_name).nil? + + write_attribute(column_name, self.class.column_defaults[column_name]) + end + end + # Validates builds and merge requests access level # which cannot be higher than repository access level def repository_children_level diff --git a/app/models/user.rb b/app/models/user.rb index 602f6e348f6..f3e8f14adf5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -337,7 +337,7 @@ class User < ApplicationRecord enum layout: { fixed: 0, fluid: 1 } # User's Dashboard preference - enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8, followed_user_activity: 9 } + enum dashboard: { projects: 0, stars: 1, your_activity: 10, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8, followed_user_activity: 9 } # User's Project preference enum project_view: { readme: 0, activity: 1, files: 2 } diff --git a/app/services/ci/runners/create_runner_service.rb b/app/services/ci/runners/create_runner_service.rb new file mode 100644 index 00000000000..2de9ee4d38e --- /dev/null +++ b/app/services/ci/runners/create_runner_service.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Ci + module Runners + class CreateRunnerService + RUNNER_CLASS_MAPPING = { + 'instance_type' => Ci::Runners::RunnerCreationStrategies::InstanceRunnerStrategy, + nil => Ci::Runners::RunnerCreationStrategies::InstanceRunnerStrategy + }.freeze + + attr_accessor :user, :type, :params, :strategy + + def initialize(user:, type:, params:) + @user = user + @type = type + @params = params + @strategy = RUNNER_CLASS_MAPPING[type].new(user: user, type: type, params: params) + end + + def execute + normalize_params + + return ServiceResponse.error(message: 'Validation error') unless strategy.validate_params + return ServiceResponse.error(message: 'Insufficient permissions') unless strategy.authorized_user? + + runner = ::Ci::Runner.new(params) + + return ServiceResponse.success(payload: { runner: runner }) if runner.save + + ServiceResponse.error(message: runner.errors.full_messages) + end + + def normalize_params + params[:registration_type] = :authenticated_user + params[:runner_type] = type + params[:active] = !params.delete(:paused) if params[:paused].present? + params[:creator] = user + + strategy.normalize_params + end + end + end +end diff --git a/app/services/ci/runners/runner_creation_strategies/instance_runner_strategy.rb b/app/services/ci/runners/runner_creation_strategies/instance_runner_strategy.rb new file mode 100644 index 00000000000..f195c3e88f9 --- /dev/null +++ b/app/services/ci/runners/runner_creation_strategies/instance_runner_strategy.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Ci + module Runners + module RunnerCreationStrategies + class InstanceRunnerStrategy + attr_accessor :user, :type, :params + + def initialize(user:, type:, params:) + @user = user + @type = type + @params = params + end + + def normalize_params + params[:runner_type] = :instance_type + end + + def validate_params + true + end + + def authorized_user? + user.present? && user.can?(:create_instance_runners) + end + end + end + end +end diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml index ca9f69ab73a..f003b4f3339 100644 --- a/app/views/dashboard/_activity_head.html.haml +++ b/app/views/dashboard/_activity_head.html.haml @@ -3,6 +3,7 @@ .top-area = gl_tabs_nav({ class: 'gl-border-b-0', data: { testid: 'dashboard-activity-tabs' } }) do - = gl_tab_link_to _("Your projects"), activity_dashboard_path, { item_active: params[:filter].nil? } + = gl_tab_link_to _("Your activity"), activity_dashboard_path, { item_active: params[:filter].nil? } + = gl_tab_link_to _("Your projects"), activity_dashboard_path(filter: 'projects') = gl_tab_link_to _("Starred projects"), activity_dashboard_path(filter: 'starred') = gl_tab_link_to _("Followed users"), activity_dashboard_path(filter: 'followed') diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index 8f802792e6a..689862eae8a 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -9,7 +9,7 @@ .tree-holder .nav-block .tree-ref-container - .tree-ref-holder + .tree-ref-holder.gl-max-w-26 #js-project-commits-ref-switcher{ data: { "project-id" => @project.id, "ref" => @ref, "commits_path": project_commits_path(@project), "ref_type": @ref_type.to_s } } %ul.breadcrumb.repo-breadcrumb diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 51555293bfb..6cd3c584f2a 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -1,7 +1,7 @@ - is_project_overview = local_assigns.fetch(:is_project_overview, false) .tree-ref-container.gl-display-flex.gl-flex-wrap.gl-gap-2.mb-2.mb-md-0 - .tree-ref-holder + .tree-ref-holder.gl-max-w-26 #js-tree-ref-switcher{ data: { project_id: @project.id, project_root_path: project_path(@project) } } #js-repo-breadcrumb{ data: breadcrumb_data_attributes } diff --git a/config/initializers/00_deprecations.rb b/config/initializers/00_deprecations.rb index 5e3ae0cd9d7..c5f9833822d 100644 --- a/config/initializers/00_deprecations.rb +++ b/config/initializers/00_deprecations.rb @@ -44,7 +44,7 @@ unless ActiveSupport::Deprecation.silenced Warning.process('', actions) # Log deprecation warnings emitted from Rails (see ActiveSupport::Deprecation). - ActiveSupport::Notifications.subscribe('deprecation.rails') do |name, start, finish, id, payload| + ActiveSupport::Notifications.subscribe('deprecation.rails') do |_name, _start, _finish, _id, payload| Gitlab::DeprecationJsonLogger.info(message: payload[:message].strip, source: 'rails') end end diff --git a/config/metrics/counts_28d/20230129134935_i_container_registry_push_tag_user_monthly.yml b/config/metrics/counts_28d/20230129134935_i_container_registry_push_tag_user_monthly.yml new file mode 100644 index 00000000000..2c2bcc689a0 --- /dev/null +++ b/config/metrics/counts_28d/20230129134935_i_container_registry_push_tag_user_monthly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_push_tag_user_monthly +description: A monthly count of users that have pushed a tag to the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_push_tag_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_28d/20230129135520_i_container_registry_delete_tag_user_monthly.yml b/config/metrics/counts_28d/20230129135520_i_container_registry_delete_tag_user_monthly.yml new file mode 100644 index 00000000000..177fc92f3f0 --- /dev/null +++ b/config/metrics/counts_28d/20230129135520_i_container_registry_delete_tag_user_monthly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_delete_tag_user_monthly +description: A monthly count of users that have deleted a tag from the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_delete_tag_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_28d/20230129135748_i_container_registry_push_repository_user_monthly.yml b/config/metrics/counts_28d/20230129135748_i_container_registry_push_repository_user_monthly.yml new file mode 100644 index 00000000000..6aa8a0fc5cb --- /dev/null +++ b/config/metrics/counts_28d/20230129135748_i_container_registry_push_repository_user_monthly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_push_repository_user_monthly +description: A monthly count of users that have pushed a repository to the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_push_repository_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_28d/20230129140646_i_container_registry_delete_repository_user_monthly.yml b/config/metrics/counts_28d/20230129140646_i_container_registry_delete_repository_user_monthly.yml new file mode 100644 index 00000000000..5dc8a351956 --- /dev/null +++ b/config/metrics/counts_28d/20230129140646_i_container_registry_delete_repository_user_monthly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_delete_repository_user_monthly +description: A monthly count of users that have deleted a repository from the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_delete_repository_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_28d/20230129140954_i_container_registry_create_repository_user_monthly.yml b/config/metrics/counts_28d/20230129140954_i_container_registry_create_repository_user_monthly.yml new file mode 100644 index 00000000000..a1789967986 --- /dev/null +++ b/config/metrics/counts_28d/20230129140954_i_container_registry_create_repository_user_monthly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_create_repository_user_monthly +description: A monthly count of users that have created a repository from the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 28d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_create_repository_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20230129032841_i_container_registry_push_tag_user_weekly.yml b/config/metrics/counts_7d/20230129032841_i_container_registry_push_tag_user_weekly.yml new file mode 100644 index 00000000000..d9c150f4b62 --- /dev/null +++ b/config/metrics/counts_7d/20230129032841_i_container_registry_push_tag_user_weekly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_push_tag_user_weekly +description: A weekly count of users that have pushed a tag to the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_push_tag_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20230129034513_i_container_registry_delete_tag_user_weekly.yml b/config/metrics/counts_7d/20230129034513_i_container_registry_delete_tag_user_weekly.yml new file mode 100644 index 00000000000..19e3866d862 --- /dev/null +++ b/config/metrics/counts_7d/20230129034513_i_container_registry_delete_tag_user_weekly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_delete_tag_user_weekly +description: A weekly count of users that have deleted a tag from the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_delete_tag_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20230129034950_i_container_registry_push_repository_user_weekly.yml b/config/metrics/counts_7d/20230129034950_i_container_registry_push_repository_user_weekly.yml new file mode 100644 index 00000000000..e719ca0abda --- /dev/null +++ b/config/metrics/counts_7d/20230129034950_i_container_registry_push_repository_user_weekly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_push_repository_user_weekly +description: A weekly count of users that have pushed a repository to the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_push_repository_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20230129035454_i_container_registry_delete_repository_user_weekly.yml b/config/metrics/counts_7d/20230129035454_i_container_registry_delete_repository_user_weekly.yml new file mode 100644 index 00000000000..46f66db821a --- /dev/null +++ b/config/metrics/counts_7d/20230129035454_i_container_registry_delete_repository_user_weekly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_delete_repository_user_weekly +description: A weekly count of users that have deleted a repository from the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_delete_repository_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/config/metrics/counts_7d/20230129035748_i_container_registry_create_repository_user_weekly.yml b/config/metrics/counts_7d/20230129035748_i_container_registry_create_repository_user_weekly.yml new file mode 100644 index 00000000000..0b20ec93c71 --- /dev/null +++ b/config/metrics/counts_7d/20230129035748_i_container_registry_create_repository_user_weekly.yml @@ -0,0 +1,26 @@ +--- +key_path: redis_hll_counters.user_container_registry.i_container_registry_create_repository_user_weekly +description: A weekly count of users that have created a repository from the registry +product_section: ops +product_stage: package +product_group: package +product_category: container_registry +value_type: number +status: active +milestone: '15.9' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107918 +time_frame: 7d +data_source: redis_hll +data_category: optional +instrumentation_class: RedisHLLMetric +options: + events: + - i_container_registry_create_repository_user +performance_indicator_type: [] +distribution: +- ce +- ee +tier: +- free +- premium +- ultimate diff --git a/doc/api/draft_notes.md b/doc/api/draft_notes.md index 7c4cd8a1e2e..a168c41092c 100644 --- a/doc/api/draft_notes.md +++ b/doc/api/draft_notes.md @@ -111,3 +111,21 @@ DELETE /projects/:id/merge_requests/:merge_request_iid/draft_notes/:draft_note_i ```shell curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/14/merge_requests/11/draft_notes/5" ``` + +## Publish a draft note + +Publishes an existing draft note for a given merge request. + +```plaintext +PUT /projects/:id/merge_requests/:merge_request_iid/draft_notes/:draft_note_id/publish +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ----------- | --------------------- | +| `draft_note_id` | integer | yes | The ID of a draft note. +| `id` | integer or string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). +| `merge_request_iid` | integer | yes | The IID of a project merge request. + +```shell +curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/14/merge_requests/11/draft_notes/5/publish" +``` diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index 68354501eba..7e581bb7419 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -120,20 +120,8 @@ While `1280px` is the standard max width when using fixed layout, some pages sti For users who have access to a large number of projects but only keep up with a select few, the amount of activity on the your dashboard can be -overwhelming. Changing this setting allows you to redefine what is displayed by default. - -You can include the following options for your default dashboard view: - -- Your projects (default) -- Starred projects -- Your projects' activity -- Starred projects' activity -- Followed Users' Activity -- Your groups -- Your [To-Do List](../todos.md) -- Assigned Issues -- Assigned Merge Requests -- [Operations Dashboard](../operations_dashboard/index.md) +overwhelming. From the **Dashboard** dropdown list, select what you'd like displayed on your +personal dashboard. ### Group overview content diff --git a/doc/user/project/merge_requests/creating_merge_requests.md b/doc/user/project/merge_requests/creating_merge_requests.md index 9dce203f328..875312bbb7c 100644 --- a/doc/user/project/merge_requests/creating_merge_requests.md +++ b/doc/user/project/merge_requests/creating_merge_requests.md @@ -35,14 +35,14 @@ If your development workflow requires an issue for every merge request, you can create a branch directly from the issue to speed the process up. The new branch, and later its merge request, are marked as related to this issue. After merging the merge request, the issue is closed automatically, unless [automatic issue closing is disabled](../issues/managing_issues.md#disable-automatic-issue-closing). -You can see a **Create merge request** dropdown below the issue description. +You can see a **Create merge request** dropdown list below the issue description. NOTE: In GitLab 14.8 and later, selecting **Create merge request** [redirects to the merge request creation form](https://gitlab.com/gitlab-org/gitlab/-/issues/349566) instead of immediately creating the merge request. -The **Create merge request** button doesn't display if: +**Create merge request** doesn't display if: - A branch with the same name already exists. - A merge request already exists for this branch. @@ -54,15 +54,14 @@ To make this button appear, one possible workaround is to After removal, the fork relationship cannot be restored. This project can no longer be able to receive or send merge requests to the source project, or other forks. -This dropdown contains the options **Create merge request and branch** and **Create branch**. +The dropdown list contains the options **Create merge request and branch** and **Create branch**. After selecting one of these options, a new branch or branch and merge request is created based on your project's [default branch](../repository/branches/default.md). -The branch name is based on an internal ID, and the issue title. The example -screenshot above creates a branch named -`2-make-static-site-auto-deploy-and-serve`. +The branch name is based on your project's branch name template. The default template +is `%{id}-%{title}`. Supported variables for branch name templates are `%{id}` and `%{title}`. -When you select the **Create branch** button in an empty +When you select **Create branch** in an empty repository project, GitLab performs these actions: - Creates a default branch. diff --git a/doc/user/project/repository/branches/default.md b/doc/user/project/repository/branches/default.md index 87caeee73e3..b33dc4450a9 100644 --- a/doc/user/project/repository/branches/default.md +++ b/doc/user/project/repository/branches/default.md @@ -44,7 +44,7 @@ To update the default branch for an individual [project](../../index.md): 1. On the top bar, select **Main menu > Projects** and find your project. 1. In the left navigation menu, go to **Settings > Repository**. -1. Expand **Default branch**. For **Initial default branch name**, select a new default branch. +1. Expand **Branch defaults**. For **Default branch**, select a new default branch. 1. Optional. Select the **Auto-close referenced issues on default branch** checkbox to close issues when a merge request [uses a closing pattern](../../issues/managing_issues.md#closing-issues-automatically). diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md index ddbb638d753..22297149561 100644 --- a/doc/user/project/service_desk.md +++ b/doc/user/project/service_desk.md @@ -80,7 +80,7 @@ WARNING: Anyone in your project can use the Service Desk email address to create an issue in this project, **regardless of their access level** to your GitLab instance. -To improve your project's security, we recommend the following: +To improve your project's security, you should: - Put the Service Desk email address behind an alias on your email system so you can change it later. - [Enable Akismet](../../integration/akismet.md) on your GitLab instance to add spam checking to this service. @@ -95,6 +95,7 @@ displayed in the information note. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2460) in GitLab 12.7. > - Moved from GitLab Premium to GitLab Free in 13.2. +> - `UNSUBSCRIBE_URL`, `SYSTEM_HEADER`, `SYSTEM_FOOTER`, and `ADDITIONAL_TEXT` placeholders [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285512) in GitLab 15.9. An email is sent to the author when: @@ -121,8 +122,6 @@ visible in the email template. For more information, see #### Thank you email -> UNSUBSCRIBE_URL, SYSTEM_HEADER, SYSTEM_FOOTER, ADDITIONAL_TEXT placeholders [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/285512) in GitLab 15.9. - When a user submits an issue through Service Desk, GitLab sends a **thank you email**. You must name the template file `thank_you.md`. diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb index 242979da367..ea9b79c12fd 100644 --- a/lib/gitlab/bitbucket_server_import/importer.rb +++ b/lib/gitlab/bitbucket_server_import/importer.rb @@ -55,7 +55,7 @@ module Gitlab handle_errors metrics.track_finished_import - log_info(stage: "complete") + log_info(import_stage: "complete") Gitlab::Cache::Import::Caching.expire(already_imported_cache_key, Gitlab::Cache::Import::Caching::SHORTER_TIMEOUT) true @@ -139,16 +139,16 @@ module Gitlab end def import_repository - log_info(stage: 'import_repository', message: 'starting import') + log_info(import_stage: 'import_repository', message: 'starting import') project.repository.import_repository(project.import_url) project.repository.fetch_as_mirror(project.import_url, refmap: self.class.refmap) - log_info(stage: 'import_repository', message: 'finished import') + log_info(import_stage: 'import_repository', message: 'finished import') rescue ::Gitlab::Git::CommandError => e Gitlab::ErrorTracking.log_exception( e, - stage: 'import_repository', message: 'failed import', error: e.message + import_stage: 'import_repository', message: 'failed import', error: e.message ) # Expire cache to prevent scenarios such as: @@ -179,10 +179,10 @@ module Gitlab def import_pull_requests page = 0 - log_info(stage: 'import_pull_requests', message: "starting") + log_info(import_stage: 'import_pull_requests', message: "starting") loop do - log_debug(stage: 'import_pull_requests', message: "importing page #{page} and batch-size #{BATCH_SIZE} from #{page * BATCH_SIZE} to #{(page + 1) * BATCH_SIZE}") + log_debug(import_stage: 'import_pull_requests', message: "importing page #{page} and batch-size #{BATCH_SIZE} from #{page * BATCH_SIZE} to #{(page + 1) * BATCH_SIZE}") pull_requests = client.pull_requests(project_key, repository_slug, page_offset: page, limit: BATCH_SIZE).to_a @@ -196,21 +196,21 @@ module Gitlab pull_requests.each do |pull_request| if already_imported?(pull_request) - log_info(stage: 'import_pull_requests', message: 'already imported', iid: pull_request.iid) + log_info(import_stage: 'import_pull_requests', message: 'already imported', iid: pull_request.iid) else import_bitbucket_pull_request(pull_request) end rescue StandardError => e Gitlab::ErrorTracking.log_exception( e, - stage: 'import_pull_requests', iid: pull_request.iid, error: e.message + import_stage: 'import_pull_requests', iid: pull_request.iid, error: e.message ) backtrace = Gitlab::BacktraceCleaner.clean_backtrace(e.backtrace) errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, backtrace: backtrace.join("\n"), raw_response: pull_request.raw } end - log_debug(stage: 'import_pull_requests', message: "finished page #{page} and batch-size #{BATCH_SIZE}") + log_debug(import_stage: 'import_pull_requests', message: "finished page #{page} and batch-size #{BATCH_SIZE}") page += 1 end end @@ -235,7 +235,7 @@ module Gitlab rescue BitbucketServer::Connection::ConnectionError => e Gitlab::ErrorTracking.log_exception( e, - stage: 'delete_temp_branches', branch: branch.name, error: e.message + import_stage: 'delete_temp_branches', branch: branch.name, error: e.message ) @errors << { type: :delete_temp_branches, branch_name: branch.name, errors: e.message } @@ -243,7 +243,7 @@ module Gitlab end def import_bitbucket_pull_request(pull_request) - log_info(stage: 'import_bitbucket_pull_requests', message: 'starting', iid: pull_request.iid) + log_info(import_stage: 'import_bitbucket_pull_requests', message: 'starting', iid: pull_request.iid) description = '' description += author_line(pull_request) @@ -274,12 +274,12 @@ module Gitlab metrics.merge_requests_counter.increment end - log_info(stage: 'import_bitbucket_pull_requests', message: 'finished', iid: pull_request.iid) + log_info(import_stage: 'import_bitbucket_pull_requests', message: 'finished', iid: pull_request.iid) mark_as_imported(pull_request) end def import_pull_request_comments(pull_request, merge_request) - log_info(stage: 'import_pull_request_comments', message: 'starting', iid: merge_request.iid) + log_info(import_stage: 'import_pull_request_comments', message: 'starting', iid: merge_request.iid) comments, other_activities = client.activities(project_key, repository_slug, pull_request.iid).partition(&:comment?) @@ -291,7 +291,7 @@ module Gitlab import_inline_comments(inline_comments.map(&:comment), merge_request) import_standalone_pr_comments(pr_comments.map(&:comment), merge_request) - log_info(stage: 'import_pull_request_comments', message: 'finished', iid: merge_request.iid, + log_info(import_stage: 'import_pull_request_comments', message: 'finished', iid: merge_request.iid, merge_event_found: merge_event.present?, inline_comments_count: inline_comments.count, standalone_pr_comments: pr_comments.count) @@ -299,7 +299,7 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def import_merge_event(merge_request, merge_event) - log_info(stage: 'import_merge_event', message: 'starting', iid: merge_request.iid) + log_info(import_stage: 'import_merge_event', message: 'starting', iid: merge_request.iid) committer = merge_event.committer_email @@ -309,12 +309,12 @@ module Gitlab metric = MergeRequest::Metrics.find_or_initialize_by(merge_request: merge_request) metric.update(merged_by_id: user_id, merged_at: timestamp) - log_info(stage: 'import_merge_event', message: 'finished', iid: merge_request.iid) + log_info(import_stage: 'import_merge_event', message: 'finished', iid: merge_request.iid) end # rubocop: enable CodeReuse/ActiveRecord def import_inline_comments(inline_comments, merge_request) - log_info(stage: 'import_inline_comments', message: 'starting', iid: merge_request.iid) + log_info(import_stage: 'import_inline_comments', message: 'starting', iid: merge_request.iid) inline_comments.each do |comment| position = build_position(merge_request, comment) @@ -329,7 +329,7 @@ module Gitlab end end - log_info(stage: 'import_inline_comments', message: 'finished', iid: merge_request.iid) + log_info(import_stage: 'import_inline_comments', message: 'finished', iid: merge_request.iid) end def create_diff_note(merge_request, comment, position, discussion_id = nil) @@ -344,7 +344,7 @@ module Gitlab return note end - log_info(stage: 'create_diff_note', message: 'creating fallback DiffNote', iid: merge_request.iid) + log_info(import_stage: 'create_diff_note', message: 'creating fallback DiffNote', iid: merge_request.iid) # Bitbucket Server supports the ability to comment on any line, not just the # line in the diff. If we can't add the note as a DiffNote, fallback to creating @@ -353,7 +353,7 @@ module Gitlab rescue StandardError => e Gitlab::ErrorTracking.log_exception( e, - stage: 'create_diff_note', comment_id: comment.id, error: e.message + import_stage: 'create_diff_note', comment_id: comment.id, error: e.message ) errors << { type: :pull_request, id: comment.id, errors: e.message } @@ -394,7 +394,7 @@ module Gitlab rescue StandardError => e Gitlab::ErrorTracking.log_exception( e, - stage: 'import_standalone_pr_comments', merge_request_id: merge_request.id, comment_id: comment.id, error: e.message + import_stage: 'import_standalone_pr_comments', merge_request_id: merge_request.id, comment_id: comment.id, error: e.message ) errors << { type: :pull_request, comment_id: comment.id, errors: e.message } diff --git a/lib/gitlab/ci/config/external/file/artifact.rb b/lib/gitlab/ci/config/external/file/artifact.rb index 8d557463a9a..0b90d240a15 100644 --- a/lib/gitlab/ci/config/external/file/artifact.rb +++ b/lib/gitlab/ci/config/external/file/artifact.rb @@ -20,8 +20,6 @@ module Gitlab def content strong_memoize(:content) do - next unless artifact_job - Gitlab::Ci::ArtifactFileReader.new(artifact_job).read(location) rescue Gitlab::Ci::ArtifactFileReader::Error => error errors.push(error.message) @@ -56,8 +54,6 @@ module Gitlab def artifact_job strong_memoize(:artifact_job) do - next unless creating_child_pipeline? - context.parent_pipeline.find_job_with_archive_artifacts(job_name) end end diff --git a/lib/gitlab/ci/config/external/file/local.rb b/lib/gitlab/ci/config/external/file/local.rb index 95206233b2e..bb1c304d02b 100644 --- a/lib/gitlab/ci/config/external/file/local.rb +++ b/lib/gitlab/ci/config/external/file/local.rb @@ -71,6 +71,8 @@ module Gitlab end def masked_blob + return unless valid? + strong_memoize(:masked_blob) do context.mask_variables_from( Gitlab::Routing.url_helpers.project_blob_url(context.project, ::File.join(context.sha, location)) @@ -79,7 +81,7 @@ module Gitlab end def masked_raw - return unless context.project + return unless valid? strong_memoize(:masked_raw) do context.mask_variables_from( diff --git a/lib/gitlab/ci/config/external/file/project.rb b/lib/gitlab/ci/config/external/file/project.rb index 248959a0df7..f8d4cb27710 100644 --- a/lib/gitlab/ci/config/external/file/project.rb +++ b/lib/gitlab/ci/config/external/file/project.rb @@ -72,9 +72,6 @@ module Gitlab end def fetch_local_content - return unless can_access_local_content? - return unless sha - BatchLoader.for([sha, location]) .batch(key: project) do |locations, loader, args| context.logger.instrument(:config_file_fetch_project_content) do @@ -88,8 +85,6 @@ module Gitlab end def sha - return unless project - strong_memoize(:sha) do project.commit(ref_name).try(:sha) end @@ -119,7 +114,7 @@ module Gitlab end def masked_blob - return unless project + return unless valid? strong_memoize(:masked_blob) do context.mask_variables_from( @@ -129,7 +124,7 @@ module Gitlab end def masked_raw - return unless project + return unless valid? strong_memoize(:masked_raw) do context.mask_variables_from( diff --git a/lib/gitlab/pages/cache_control.rb b/lib/gitlab/pages/cache_control.rb index a0b3ab87f38..81da34f1219 100644 --- a/lib/gitlab/pages/cache_control.rb +++ b/lib/gitlab/pages/cache_control.rb @@ -52,9 +52,9 @@ module Gitlab ::Gitlab::AppLogger.info( message: 'clear pages cache', - keys: keys, - type: @type, - id: @id + pages_keys: keys, + pages_type: @type, + pages_id: @id ) Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do diff --git a/lib/gitlab/usage_data_counters/known_events/container_registry_events.yml b/lib/gitlab/usage_data_counters/known_events/container_registry_events.yml new file mode 100644 index 00000000000..e8b14de1769 --- /dev/null +++ b/lib/gitlab/usage_data_counters/known_events/container_registry_events.yml @@ -0,0 +1,22 @@ +--- +- name: i_container_registry_push_tag_user + category: user_container_registry + aggregation: weekly + redis_slot: container_registry +- name: i_container_registry_delete_tag_user + category: user_container_registry + aggregation: weekly + redis_slot: container_registry +- name: i_container_registry_push_repository_user + category: user_container_registry + aggregation: weekly + redis_slot: container_registry +- name: i_container_registry_delete_repository_user + category: user_container_registry + aggregation: weekly + redis_slot: container_registry +- name: i_container_registry_create_repository_user + category: user_container_registry + aggregation: weekly + redis_slot: container_registry +
\ No newline at end of file diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ef899e10756..3d91bcd308c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -16501,6 +16501,9 @@ msgstr "" msgid "EscalationPolicies|Choose who to email if those contacted first about an alert don't respond." msgstr "" +msgid "EscalationPolicies|Choose who to email if those contacted first about an alert don't respond. To access this feature, ask %{linkStart}a project Owner%{linkEnd} to grant you at least the Maintainer role." +msgstr "" + msgid "EscalationPolicies|Create an escalation policy in GitLab" msgstr "" @@ -26455,18 +26458,9 @@ msgstr "" msgid "Merge blocked: all merge request dependencies must be merged." msgstr "" -msgid "Merge blocked: merge request must be marked as ready. It's still marked as draft." -msgstr "" - -msgid "Merge blocked: new changes were just added." -msgstr "" - msgid "Merge blocked: pipeline must succeed. It's waiting for a manual job to continue." msgstr "" -msgid "Merge blocked: the source branch must be rebased onto the target branch." -msgstr "" - msgid "Merge commit SHA" msgstr "" @@ -26539,9 +26533,6 @@ msgstr "" msgid "Merge requests can't be merged if the status checks did not succeed or are still running." msgstr "" -msgid "Merge unavailable: merge requests are read-only in a secondary Geo node." -msgstr "" - msgid "Merge unverified changes" msgstr "" @@ -35137,24 +35128,15 @@ msgstr "" msgid "Reauthenticating with SAML provider." msgstr "" -msgid "Rebase" -msgstr "" - msgid "Rebase completed" msgstr "" -msgid "Rebase in progress" -msgstr "" - msgid "Rebase source branch" msgstr "" msgid "Rebase source branch on the target branch." msgstr "" -msgid "Rebase without pipeline" -msgstr "" - msgid "Recaptcha verified?" msgstr "" @@ -49353,6 +49335,9 @@ msgstr "" msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}." msgstr "" +msgid "Your Activity" +msgstr "" + msgid "Your CI/CD configuration syntax is invalid. Select the Validate tab for more details." msgstr "" @@ -49467,6 +49452,9 @@ msgstr "" msgid "Your action succeeded." msgstr "" +msgid "Your activity" +msgstr "" + msgid "Your applications (%{size})" msgstr "" @@ -50916,10 +50904,16 @@ msgstr "" msgid "manual" msgstr "" -msgid "math|Displaying this math block may cause performance issues on this page" +msgid "math|Displaying this math block may cause performance issues on this page." +msgstr "" + +msgid "math|There was an error rendering this math block. %{katexMessage}" msgstr "" -msgid "math|There was an error rendering this math block" +msgid "math|This math block exceeds %{maxMathChars} characters, and may cause performance issues on this page." +msgstr "" + +msgid "math|Too many expansions. Consider using multiple math blocks." msgstr "" msgid "member" @@ -50977,6 +50971,84 @@ msgstr "" msgid "mrWidget|%{boldHeaderStart}Looks like there's no pipeline here.%{boldHeaderEnd}" msgstr "" +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} Select %{boldStart}Mark as ready%{boldEnd} to remove it from Draft status." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} Users who can write to the source or target branches can resolve the conflicts." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} a Jira issue key must be mentioned in the title or description." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} all required approvals must be given." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} all status checks must pass." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} all threads must be resolved." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} denied licenses must be removed." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} fast-forward merge is not possible. To merge this request, first rebase locally." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} merge conflicts must be resolved." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} new changes were just added." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. It's waiting for a manual action to continue." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} pipeline must succeed. Push a commit that fixes the failure or %{linkStart}learn about other solutions.%{linkEnd}" +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} the source branch must be rebased onto the target branch." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge blocked:%{boldEnd} you can only merge after the above items are resolved." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge unavailable:%{boldEnd} merge requests are read-only in a secondary Geo node." +msgstr "" + +msgid "mrWidget|%{boldStart}Merge unavailable:%{boldEnd} merge requests are read-only on archived projects." +msgstr "" + +msgid "mrWidget|%{boldStart}Merging!%{boldEnd} Changes are being shipped…" +msgstr "" + +msgid "mrWidget|%{boldStart}Merging!%{boldEnd} Changes will land soon…" +msgstr "" + +msgid "mrWidget|%{boldStart}Merging!%{boldEnd} Drum roll, please…" +msgstr "" + +msgid "mrWidget|%{boldStart}Merging!%{boldEnd} Everything's good…" +msgstr "" + +msgid "mrWidget|%{boldStart}Merging!%{boldEnd} Lift-off in 5… 4… 3…" +msgstr "" + +msgid "mrWidget|%{boldStart}Merging!%{boldEnd} Take a deep breath and relax…" +msgstr "" + +msgid "mrWidget|%{boldStart}Merging!%{boldEnd} The changes are leaving the station…" +msgstr "" + +msgid "mrWidget|%{boldStart}Merging!%{boldEnd} This is going to be great…" +msgstr "" + +msgid "mrWidget|%{boldStart}Merging!%{boldEnd} We're almost there…" +msgstr "" + +msgid "mrWidget|%{boldStart}Ready to be merged automatically.%{boldEnd} Ask someone with write access to this repository to merge this request." +msgstr "" + msgid "mrWidget|%{mergeError}." msgstr "" @@ -51119,82 +51191,31 @@ msgid_plural "mrWidget|Mentions issues" msgstr[0] "" msgstr[1] "" -msgid "mrWidget|Merge blocked: a Jira issue key must be mentioned in the title or description." -msgstr "" - msgid "mrWidget|Merge blocked: all required approvals must be given." msgstr "" -msgid "mrWidget|Merge blocked: all status checks must pass." -msgstr "" - -msgid "mrWidget|Merge blocked: all threads must be resolved." -msgstr "" - -msgid "mrWidget|Merge blocked: denied licenses must be removed." -msgstr "" - -msgid "mrWidget|Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally." -msgstr "" - -msgid "mrWidget|Merge blocked: merge conflicts must be resolved." -msgstr "" - -msgid "mrWidget|Merge blocked: pipeline must succeed. It's waiting for a manual action to continue." -msgstr "" - -msgid "mrWidget|Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or %{linkStart}learn about other solutions.%{linkEnd}" -msgstr "" - -msgid "mrWidget|Merge blocked: you can only merge after the above items are resolved." -msgstr "" - msgid "mrWidget|Merge failed." msgstr "" -msgid "mrWidget|Merge unavailable: merge requests are read-only on archived projects." -msgstr "" - msgid "mrWidget|Merged by" msgstr "" -msgid "mrWidget|Merging! Changes are being shipped…" -msgstr "" - -msgid "mrWidget|Merging! Changes will land soon…" -msgstr "" - -msgid "mrWidget|Merging! Drum roll, please…" -msgstr "" - -msgid "mrWidget|Merging! Everything's good…" -msgstr "" - -msgid "mrWidget|Merging! Lift-off in 5… 4… 3…" -msgstr "" - -msgid "mrWidget|Merging! Take a deep breath and relax…" -msgstr "" - -msgid "mrWidget|Merging! The changes are leaving the station…" -msgstr "" - -msgid "mrWidget|Merging! This is going to be great…" +msgid "mrWidget|More information" msgstr "" -msgid "mrWidget|Merging! We're almost there…" +msgid "mrWidget|No users match the rule's criteria." msgstr "" -msgid "mrWidget|More information" +msgid "mrWidget|Please restore it or use a different %{type} branch." msgstr "" -msgid "mrWidget|No users match the rule's criteria." +msgid "mrWidget|Rebase" msgstr "" -msgid "mrWidget|Please restore it or use a different %{type} branch." +msgid "mrWidget|Rebase in progress" msgstr "" -msgid "mrWidget|Ready to be merged automatically. Ask someone with write access to this repository to merge this request" +msgid "mrWidget|Rebase without pipeline" msgstr "" msgid "mrWidget|Refresh" @@ -51257,9 +51278,6 @@ msgstr "" msgid "mrWidget|To change this default message, edit the template for squash commit messages. %{linkStart}Learn more.%{linkEnd}" msgstr "" -msgid "mrWidget|Users who can write to the source or target branches can resolve the conflicts." -msgstr "" - msgid "mrWidget|What is a merge train?" msgstr "" diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb index efeff7af9ab..304e08f40bd 100644 --- a/spec/controllers/dashboard_controller_spec.rb +++ b/spec/controllers/dashboard_controller_spec.rb @@ -111,9 +111,9 @@ RSpec.describe DashboardController, feature_category: :code_review_workflow do include DesignManagementTestHelpers render_views - let(:user) { create(:user) } - let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) } - let(:other_project) { create(:project, :public) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) } + let_it_be(:other_project) { create(:project, :public) } before do enable_design_management @@ -134,22 +134,53 @@ RSpec.describe DashboardController, feature_category: :code_review_workflow do other_project.add_developer(user) end - it 'returns count' do - get :activity, params: { format: :json } + context 'without filter param' do + it 'returns only events of the user' do + get :activity, params: { format: :json } + + expect(json_response['count']).to eq(3) + end + end + + context 'with "projects" filter' do + it 'returns events of the user\'s projects' do + get :activity, params: { format: :json, filter: :projects } - expect(json_response['count']).to eq(6) + expect(json_response['count']).to eq(6) + end + end + + context 'with "followed" filter' do + let_it_be(:followed_user) { create(:user) } + let_it_be(:followed_user_private_project) { create(:project, :private) } + let_it_be(:followed_user_public_project) { create(:project, :public) } + + before do + followed_user_private_project.add_developer(followed_user) + followed_user_public_project.add_developer(followed_user) + user.follow(followed_user) + create(:event, :created, project: followed_user_private_project, target: create(:issue), + author: followed_user) + create(:event, :created, project: followed_user_public_project, target: create(:issue), author: followed_user) + end + + it 'returns public events of the user\'s followed users' do + get :activity, params: { format: :json, filter: :followed } + + expect(json_response['count']).to eq(1) + end end end context 'when user has no permission to see the event' do it 'filters out invisible event' do - get :activity, params: { format: :json } + get :activity, params: { format: :json, filter: :projects } expect(json_response['html']).to include(_('No activities found')) end it 'filters out invisible event when calculating the count' do - get :activity, params: { format: :json } + get :activity, params: { format: :json, filter: :projects } expect(json_response['count']).to eq(0) end diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb index c6a8cee2f70..6fa1d93265d 100644 --- a/spec/controllers/root_controller_spec.rb +++ b/spec/controllers/root_controller_spec.rb @@ -37,16 +37,16 @@ RSpec.describe RootController do user.dashboard = 'stars' end - it 'redirects to their specified dashboard' do + it 'redirects to their starred projects list' do get :index expect(response).to redirect_to starred_dashboard_projects_path end end - context 'who has customized their dashboard setting for project activities' do + context 'who has customized their dashboard setting for their own activities' do before do - user.dashboard = 'project_activity' + user.dashboard = 'your_activity' end it 'redirects to the activity list' do @@ -56,12 +56,24 @@ RSpec.describe RootController do end end + context 'who has customized their dashboard setting for project activities' do + before do + user.dashboard = 'project_activity' + end + + it 'redirects to the projects activity list' do + get :index + + expect(response).to redirect_to activity_dashboard_path(filter: 'projects') + end + end + context 'who has customized their dashboard setting for starred project activities' do before do user.dashboard = 'starred_project_activity' end - it 'redirects to the activity list' do + it 'redirects to their starred projects activity list' do get :index expect(response).to redirect_to activity_dashboard_path(filter: 'starred') @@ -73,7 +85,7 @@ RSpec.describe RootController do user.dashboard = 'followed_user_activity' end - it 'redirects to the activity list' do + it 'redirects to the followed users activity list' do get :index expect(response).to redirect_to activity_dashboard_path(filter: 'followed') diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb index ec6f67a59a8..2f9b7bb7e0f 100644 --- a/spec/features/dashboard/activity_spec.rb +++ b/spec/features/dashboard/activity_spec.rb @@ -12,9 +12,15 @@ RSpec.describe 'Dashboard > Activity', feature_category: :user_profile do it_behaves_like 'a dashboard page with sidebar', :activity_dashboard_path, :activity context 'tabs' do - it 'shows Your Projects' do + it 'shows Your Activity' do visit activity_dashboard_path + expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Your activity') + end + + it 'shows Your Projects' do + visit activity_dashboard_path(filter: 'projects') + expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Your projects') end @@ -24,7 +30,7 @@ RSpec.describe 'Dashboard > Activity', feature_category: :user_profile do expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Starred projects') end - it 'shows Followed Projects' do + it 'shows Followed Users' do visit activity_dashboard_path(filter: 'followed') expect(find('[data-testid="dashboard-activity-tabs"] a.active')).to have_content('Followed users') diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb index 0c77bd2a8ff..25459494a0c 100644 --- a/spec/features/markdown/math_spec.rb +++ b/spec/features/markdown/math_spec.rb @@ -64,7 +64,82 @@ RSpec.describe 'Math rendering', :js, feature_category: :team_planning do visit project_issue_path(project, issue) page.within '.description > .md' do - expect(page).to have_selector('.js-lazy-render-math') + expect(page).to have_selector('.js-lazy-render-math-container', text: /math block exceeds 1000 characters/) + end + end + + it 'allows many expansions', :js do + description = <<~MATH + ```math + #{'\\mod e ' * 100} + ``` + MATH + + issue = create(:issue, project: project, description: description) + + visit project_issue_path(project, issue) + + wait_for_requests + + page.within '.description > .md' do + expect(page).not_to have_selector('.katex-error') + end + end + + it 'shows error message when too many expansions', :js do + description = <<~MATH + ```math + #{'\\mod e ' * 150} + ``` + MATH + + issue = create(:issue, project: project, description: description) + + visit project_issue_path(project, issue) + + wait_for_requests + + page.within '.description > .md' do + click_button 'Display anyway' + + expect(page).to have_selector('.katex-error', text: /Too many expansions/) + end + end + + it 'shows error message when other errors are generated', :js do + description = <<~MATH + ```math + \\unknown + ``` + MATH + + issue = create(:issue, project: project, description: description) + + visit project_issue_path(project, issue) + + wait_for_requests + + page.within '.description > .md' do + expect(page).to have_selector('.katex-error', + text: /There was an error rendering this math block. KaTeX parse error/) + end + end + + it 'escapes HTML in error', :js do + description = <<~MATH + ```math + \\unknown <script> + ``` + MATH + + issue = create(:issue, project: project, description: description) + + visit project_issue_path(project, issue) + + wait_for_requests + + page.within '.description > .md' do + expect(page).to have_selector('.katex-error', text: /&lt;script&gt;/) end end diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb index 79c166434aa..d47968ebc6b 100644 --- a/spec/features/merge_request/user_merges_immediately_spec.rb +++ b/spec/features/merge_request/user_merges_immediately_spec.rb @@ -38,7 +38,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js, feature_category end end - expect(find('.media-body h4')).to have_content('Merging!') + expect(find('[data-testid="merging-state"]')).to have_content('Merging!') wait_for_requests end diff --git a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb index c73ba1bdbe5..cdc00017ab3 100644 --- a/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb @@ -57,7 +57,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea wait_for_requests expect(page).not_to have_button('Merge') - expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or learn about other solutions.') + expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.') end end @@ -70,7 +70,7 @@ RSpec.describe 'Merge request > User merges only if pipeline succeeds', :js, fea wait_for_requests expect(page).not_to have_button 'Merge' - expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure, or learn about other solutions.') + expect(page).to have_content('Merge blocked: pipeline must succeed. Push a commit that fixes the failure or learn about other solutions.') end end diff --git a/spec/features/merge_request/user_resolves_wip_mr_spec.rb b/spec/features/merge_request/user_resolves_wip_mr_spec.rb index 8a19a72f6ae..01cc6bd5167 100644 --- a/spec/features/merge_request/user_resolves_wip_mr_spec.rb +++ b/spec/features/merge_request/user_resolves_wip_mr_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'Merge request > User resolves Draft', :js, feature_category: :co it 'retains merge request data after clicking Resolve WIP status' do expect(page.find('.ci-widget-content')).to have_content("Pipeline ##{pipeline.id}") - expect(page).to have_content "Merge blocked: merge request must be marked as ready. It's still marked as draft." + expect(page).to have_content "Merge blocked: Select Mark as ready to remove it from Draft status." page.within('.mr-state-widget') do click_button('Mark as ready') @@ -45,7 +45,7 @@ RSpec.describe 'Merge request > User resolves Draft', :js, feature_category: :co # merge request widget refreshes, which masks missing elements # that should already be present. expect(page.find('.ci-widget-content', wait: 0)).to have_content("Pipeline ##{pipeline.id}") - expect(page).not_to have_content("Merge blocked: merge request must be marked as ready. It's still marked as draft.") + expect(page).not_to have_content("Merge blocked: Select Mark as ready to remove it from Draft status.") end end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index a93242c0198..acf2893b513 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -148,7 +148,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js, feature_category: click_button 'Merge unverified changes' end - expect(find('.media-body h4')).to have_content('Merging!') + expect(find('[data-testid="merging-state"]')).to have_content('Merging!') end end diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js index 3fa0780a3c4..bf208f16d18 100644 --- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js @@ -105,21 +105,18 @@ describe('MRWidget approvals', () => { }); describe('when created', () => { - beforeEach(() => { - createComponent(); - }); - - it('shows loading message', () => { - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ fetchingApprovals: true }); + it('shows loading message', async () => { + service = { + fetchApprovals: jest.fn().mockReturnValue(new Promise(() => {})), + }; - return nextTick().then(() => { - expect(wrapper.text()).toContain(FETCH_LOADING); - }); + createComponent(); + await nextTick(); + expect(wrapper.text()).toContain(FETCH_LOADING); }); it('fetches approvals', () => { + createComponent(); expect(service.fetchApprovals).toHaveBeenCalled(); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js index 7b52773e92d..ec047fe0714 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js @@ -219,18 +219,15 @@ describe('Merge request widget rebase component', () => { it('renders a message explaining user does not have permissions', () => { const text = findRebaseMessageText(); - expect(text).toContain( - 'Merge blocked: the source branch must be rebased onto the target branch.', - ); + expect(text).toContain('Merge blocked:'); expect(text).toContain('the source branch must be rebased'); }); it('renders the correct target branch name', () => { - const elem = findRebaseMessage(); + const text = findRebaseMessageText(); - expect(elem.text()).toContain( - 'Merge blocked: the source branch must be rebased onto the target branch.', - ); + expect(text).toContain('Merge blocked:'); + expect(text).toContain('the source branch must be rebased onto the target branch.'); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js index 8eeba4d6274..e4448346685 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import MergeChecksFailed from '~/vue_merge_request_widget/components/states/merge_checks_failed.vue'; import { DETAILED_MERGE_STATUS } from '~/vue_merge_request_widget/constants'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; let wrapper; @@ -23,6 +24,7 @@ describe('Merge request widget merge checks failed state component', () => { `('display $displayText text for $mrState', ({ mrState, displayText }) => { factory({ mr: mrState }); - expect(wrapper.text()).toContain(MergeChecksFailed.i18n[displayText]); + const message = wrapper.findComponent(BoldText).props('message'); + expect(message).toContain(MergeChecksFailed.i18n[displayText]); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js index 5c07f4ce143..08700e834d7 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue'; import StateContainer from '~/vue_merge_request_widget/components/state_container.vue'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; describe('MRWidgetArchived', () => { let wrapper; @@ -20,8 +21,8 @@ describe('MRWidgetArchived', () => { }); it('renders information about merging', () => { - expect(wrapper.text()).toContain( - 'Merge unavailable: merge requests are read-only on archived projects.', - ); + const message = wrapper.findComponent(BoldText).props('message'); + expect(message).toContain('Merge unavailable:'); + expect(message).toContain('merge requests are read-only on archived projects.'); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js index a16e4d4a6ea..f13fc8a5007 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js @@ -12,9 +12,9 @@ describe('MRWidgetConflicts', () => { const findResolveButton = () => wrapper.findByTestId('resolve-conflicts-button'); const findMergeLocalButton = () => wrapper.findByTestId('merge-locally-button'); - const mergeConflictsText = 'Merge blocked: merge conflicts must be resolved.'; + const mergeConflictsText = 'merge conflicts must be resolved.'; const fastForwardMergeText = - 'Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.'; + 'fast-forward merge is not possible. To merge this request, first rebase locally.'; const userCannotMergeText = 'Users who can write to the source or target branches can resolve the conflicts.'; const resolveConflictsBtnText = 'Resolve conflicts'; @@ -76,8 +76,9 @@ describe('MRWidgetConflicts', () => { }); it('should tell you about conflicts without bothering other people', () => { - expect(wrapper.text()).toContain(mergeConflictsText); - expect(wrapper.text()).not.toContain(userCannotMergeText); + const text = removeBreakLine(wrapper.text()).trim(); + expect(text).toContain(mergeConflictsText); + expect(text).not.toContain(userCannotMergeText); }); it('should not allow you to resolve the conflicts', () => { @@ -102,8 +103,9 @@ describe('MRWidgetConflicts', () => { }); it('should tell you about conflicts', () => { - expect(wrapper.text()).toContain(mergeConflictsText); - expect(wrapper.text()).toContain(userCannotMergeText); + const text = removeBreakLine(wrapper.text()).trim(); + expect(text).toContain(mergeConflictsText); + expect(text).toContain(userCannotMergeText); }); it('should allow you to resolve the conflicts', () => { @@ -129,8 +131,9 @@ describe('MRWidgetConflicts', () => { }); it('should tell you about conflicts without bothering other people', () => { - expect(wrapper.text()).toContain(mergeConflictsText); - expect(wrapper.text()).not.toContain(userCannotMergeText); + const text = removeBreakLine(wrapper.text()).trim(); + expect(text).toContain(mergeConflictsText); + expect(text).not.toContain(userCannotMergeText); }); it('should allow you to resolve the conflicts', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js index 49bd3739fdb..5408f731b34 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import simplePoll from '~/lib/utils/simple_poll'; import MrWidgetMerging from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; jest.mock('~/lib/utils/simple_poll', () => jest.fn().mockImplementation(jest.requireActual('~/lib/utils/simple_poll').default), @@ -33,14 +34,8 @@ describe('MRWidgetMerging', () => { }); it('renders information about merge request being merged', () => { - expect( - wrapper - .find('.media-body') - .text() - .trim() - .replace(/\s\s+/g, ' ') - .replace(/[\r\n]+/g, ' '), - ).toContain('Merging!'); + const message = wrapper.findComponent(BoldText).props('message'); + expect(message).toContain('Merging!'); }); describe('initiateMergePolling', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js index c6e7198c678..42515c597c5 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue'; import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; describe('MRWidgetNotAllowed', () => { let wrapper; @@ -20,8 +21,9 @@ describe('MRWidgetNotAllowed', () => { }); it('renders informative text', () => { - expect(wrapper.text()).toContain('Ready to be merged automatically.'); - expect(wrapper.text()).toContain( + const message = wrapper.findComponent(BoldText).props('message'); + expect(message).toContain('Ready to be merged automatically.'); + expect(message).toContain( 'Ask someone with write access to this repository to merge this request', ); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js index 4219ad70b4c..c0197b5e20a 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import PipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue'; import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; describe('MRWidgetPipelineBlocked', () => { let wrapper; @@ -20,8 +21,10 @@ describe('MRWidgetPipelineBlocked', () => { }); it('renders information text', () => { - expect(wrapper.text()).toBe( - "Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.", + const message = wrapper.findComponent(BoldText).props('message'); + expect(message).toContain('Merge blocked:'); + expect(message).toContain( + "pipeline must succeed. It's waiting for a manual action to continue.", ); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js index bd158d59d74..8bae2b62ed1 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js @@ -1,7 +1,9 @@ import { GlSprintf, GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { removeBreakLine } from 'helpers/text_helper'; import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue'; import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; +import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; describe('PipelineFailed', () => { let wrapper; @@ -32,16 +34,20 @@ describe('PipelineFailed', () => { it('should render error message with a disabled merge button', () => { createComponent(); - expect(wrapper.text()).toContain('Merge blocked: pipeline must succeed.'); - expect(wrapper.text()).toContain('Push a commit that fixes the failure'); + const text = removeBreakLine(wrapper.text()).trim(); + expect(text).toContain('Merge blocked:'); + expect(text).toContain('pipeline must succeed'); + expect(text).toContain('Push a commit that fixes the failure'); expect(wrapper.findComponent(GlLink).text()).toContain('learn about other solutions'); }); it('should render pipeline blocked message', () => { createComponent({ isPipelineBlocked: true }); - expect(wrapper.text()).toContain( - "Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.", + const message = wrapper.findComponent(BoldText).props('message'); + expect(message).toContain('Merge blocked:'); + expect(message).toContain( + "pipeline must succeed. It's waiting for a manual action to continue.", ); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js index 2a343997cf5..aaa4591d67d 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js @@ -25,7 +25,7 @@ describe('ShaMismatch', () => { }); it('should render warning message', () => { - expect(wrapper.element.innerText).toContain(I18N_SHA_MISMATCH.warningMessage); + expect(wrapper.text()).toContain('Merge blocked: new changes were just added.'); }); it('action button should have correct label', () => { diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js index e2d79c61b9b..c97b42f61ac 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; +import { removeBreakLine } from 'helpers/text_helper'; import notesEventHub from '~/notes/event_hub'; import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue'; @@ -42,7 +43,9 @@ describe('UnresolvedDiscussions', () => { }); it('should have correct elements', () => { - expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`); + const text = removeBreakLine(wrapper.text()).trim(); + expect(text).toContain('Merge blocked:'); + expect(text).toContain('all threads must be resolved.'); expect(wrapper.element.innerText).toContain('Jump to first unresolved thread'); expect(wrapper.element.innerText).toContain('Create issue to resolve all threads'); @@ -54,7 +57,9 @@ describe('UnresolvedDiscussions', () => { describe('without threads path', () => { it('should not show create issue link if user cannot create issue', () => { - expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`); + const text = removeBreakLine(wrapper.text()).trim(); + expect(text).toContain('Merge blocked:'); + expect(text).toContain('all threads must be resolved.'); expect(wrapper.element.innerText).toContain('Jump to first unresolved thread'); expect(wrapper.element.innerText).not.toContain('Create issue to resolve all threads'); diff --git a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js index 72c61b99cf4..e610ceb2122 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js @@ -4,7 +4,6 @@ import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_stat import { createAlert } from '~/flash'; import WorkInProgress, { MSG_SOMETHING_WENT_WRONG, - MSG_MERGE_BLOCKED, MSG_MARK_READY, } from '~/vue_merge_request_widget/components/states/work_in_progress.vue'; import draftQuery from '~/vue_merge_request_widget/queries/states/draft.query.graphql'; @@ -110,7 +109,9 @@ describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', () }); it('renders text', () => { - expect(wrapper.text()).toContain(MSG_MERGE_BLOCKED); + const message = wrapper.text(); + expect(message).toContain('Merge blocked:'); + expect(message).toContain('Select Mark as ready to remove it from Draft status.'); }); it('renders mark ready button', () => { diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js index 4ade168aa84..30a7439579f 100644 --- a/spec/frontend/vue_shared/components/url_sync_spec.js +++ b/spec/frontend/vue_shared/components/url_sync_spec.js @@ -1,7 +1,10 @@ import { shallowMount } from '@vue/test-utils'; -import { historyPushState } from '~/lib/utils/common_utils'; +import { historyPushState, historyReplaceState } from '~/lib/utils/common_utils'; import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility'; -import UrlSyncComponent, { URL_SET_PARAMS_STRATEGY } from '~/vue_shared/components/url_sync.vue'; +import UrlSyncComponent, { + URL_SET_PARAMS_STRATEGY, + HISTORY_REPLACE_UPDATE_METHOD, +} from '~/vue_shared/components/url_sync.vue'; jest.mock('~/lib/utils/url_utility', () => ({ mergeUrlParams: jest.fn((query, url) => `urlParams: ${JSON.stringify(query)} ${url}`), @@ -10,6 +13,7 @@ jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/common_utils', () => ({ historyPushState: jest.fn(), + historyReplaceState: jest.fn(), })); describe('url sync component', () => { @@ -18,14 +22,12 @@ describe('url sync component', () => { const findButton = () => wrapper.find('button'); - const createComponent = ({ - query = mockQuery, - scopedSlots, - slots, - urlParamsUpdateStrategy, - } = {}) => { + const createComponent = ({ props = {}, scopedSlots, slots } = {}) => { wrapper = shallowMount(UrlSyncComponent, { - propsData: { query, ...(urlParamsUpdateStrategy && { urlParamsUpdateStrategy }) }, + propsData: { + query: mockQuery, + ...props, + }, scopedSlots, slots, }); @@ -35,14 +37,19 @@ describe('url sync component', () => { wrapper.destroy(); }); - const expectUrlSyncWithMergeUrlParams = (query, times, mergeUrlParamsReturnValue) => { + const expectUrlSyncWithMergeUrlParams = ( + query, + times, + mergeUrlParamsReturnValue, + historyMethod = historyPushState, + ) => { expect(mergeUrlParams).toHaveBeenCalledTimes(times); expect(mergeUrlParams).toHaveBeenCalledWith(query, window.location.href, { spreadArrays: true, }); - expect(historyPushState).toHaveBeenCalledTimes(times); - expect(historyPushState).toHaveBeenCalledWith(mergeUrlParamsReturnValue); + expect(historyMethod).toHaveBeenCalledTimes(times); + expect(historyMethod).toHaveBeenCalledWith(mergeUrlParamsReturnValue); }; const expectUrlSyncWithSetUrlParams = (query, times, setUrlParamsReturnValue) => { @@ -76,13 +83,32 @@ describe('url sync component', () => { describe('with url-params-update-strategy equals to URL_SET_PARAMS_STRATEGY', () => { it('uses setUrlParams to generate URL', () => { createComponent({ - urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY, + props: { + urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY, + }, }); expectUrlSyncWithSetUrlParams(mockQuery, 1, setUrlParams.mock.results[0].value); }); }); + describe('with history-update-method equals to HISTORY_REPLACE_UPDATE_METHOD', () => { + it('uses historyReplaceState to update the URL', () => { + createComponent({ + props: { + historyUpdateMethod: HISTORY_REPLACE_UPDATE_METHOD, + }, + }); + + expectUrlSyncWithMergeUrlParams( + mockQuery, + 1, + mergeUrlParams.mock.results[0].value, + historyReplaceState, + ); + }); + }); + describe('with scoped slot', () => { const scopedSlots = { default: ` @@ -91,13 +117,13 @@ describe('url sync component', () => { }; it('renders the scoped slot', () => { - createComponent({ query: null, scopedSlots }); + createComponent({ props: { query: null }, scopedSlots }); expect(findButton().exists()).toBe(true); }); it('syncs the url with the scoped slots function', () => { - createComponent({ query: null, scopedSlots }); + createComponent({ props: { query: null }, scopedSlots }); findButton().trigger('click'); @@ -111,7 +137,7 @@ describe('url sync component', () => { }; it('renders the default slot', () => { - createComponent({ query: null, slots }); + createComponent({ props: { query: null }, slots }); expect(findButton().exists()).toBe(true); }); diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 898999e328e..9d1564dfef1 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -27,6 +27,7 @@ RSpec.describe PreferencesHelper do expect(helper.dashboard_choices).to match_array [ { text: "Your Projects (default)", value: 'projects' }, { text: "Starred Projects", value: 'stars' }, + { text: "Your Activity", value: 'your_activity' }, { text: "Your Projects' Activity", value: 'project_activity' }, { text: "Starred Projects' Activity", value: 'starred_project_activity' }, { text: "Followed Users' Activity", value: 'followed_user_activity' }, diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb index ab4be5a909a..3cff2411054 100644 --- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BitbucketServerImport::Importer do +RSpec.describe Gitlab::BitbucketServerImport::Importer, feature_category: :importers do include ImportSpecHelper let(:import_url) { 'http://my-bitbucket' } diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb index 3e990d28bb2..abe38cdbc3e 100644 --- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb @@ -230,15 +230,21 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p } context 'when project name and ref include masked variables' do + let(:project_name) { 'my_project_name' } + let(:branch_name) { 'merge-commit-analyze-after' } + let(:project) { create(:project, :repository, name: project_name) } + let(:namespace_path) { project.namespace.full_path } + let(:included_project_sha) { project.commit(branch_name).sha } + let(:variables) do Gitlab::Ci::Variables::Collection.new( [ - { key: 'VAR1', value: 'a_secret_variable_value1', masked: true }, - { key: 'VAR2', value: 'a_secret_variable_value2', masked: true } + { key: 'VAR1', value: project_name, masked: true }, + { key: 'VAR2', value: branch_name, masked: true } ]) end - let(:params) { { project: 'a_secret_variable_value1', ref: 'a_secret_variable_value2', file: '/file.yml' } } + let(:params) { { project: project.full_path, ref: branch_name, file: '/file.yml' } } it { is_expected.to eq( @@ -246,9 +252,9 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p context_sha: project_sha, type: :file, location: 'file.yml', - blob: nil, - raw: nil, - extra: { project: 'xxxxxxxxxxxxxxxxxxxxxxxx', ref: 'xxxxxxxxxxxxxxxxxxxxxxxx' } + blob: "http://localhost/#{namespace_path}/xxxxxxxxxxxxxxx/-/blob/#{included_project_sha}/file.yml", + raw: "http://localhost/#{namespace_path}/xxxxxxxxxxxxxxx/-/raw/#{included_project_sha}/file.yml", + extra: { project: "#{namespace_path}/xxxxxxxxxxxxxxx", ref: 'xxxxxxxxxxxxxxxxxxxxxxxxxx' } ) } end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index c4adce1200e..252d20d9c3a 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -63,7 +63,7 @@ RSpec.describe Gitlab::GitalyClient::CommitService do end end - context 'when given a whitesapce param' do + context 'when given a whitespace param' do context 'and the param is true' do it 'uses the ignore all white spaces const' do request = Gitaly::CommitDiffRequest.new diff --git a/spec/lib/gitlab/pages/cache_control_spec.rb b/spec/lib/gitlab/pages/cache_control_spec.rb index 3ffc1998ebc..72240f52580 100644 --- a/spec/lib/gitlab/pages/cache_control_spec.rb +++ b/spec/lib/gitlab/pages/cache_control_spec.rb @@ -22,9 +22,9 @@ RSpec.describe Gitlab::Pages::CacheControl, feature_category: :pages do .to receive(:info) .with( message: 'clear pages cache', - keys: cached_keys, - type: type, - id: 1 + pages_keys: cached_keys, + pages_type: type, + pages_id: 1 ) expect(Rails.cache) diff --git a/spec/models/container_registry/event_spec.rb b/spec/models/container_registry/event_spec.rb index c2c494c49fb..07ac35f7b6a 100644 --- a/spec/models/container_registry/event_spec.rb +++ b/spec/models/container_registry/event_spec.rb @@ -116,6 +116,24 @@ RSpec.describe ContainerRegistry::Event do subject { described_class.new(raw_event).track! } + shared_examples 'tracking event is sent to HLLRedisCounter with event and originator ID' do |originator_type| + it 'fetches the event originator based on username' do + count.times do + expect(User).to receive(:find_by_username).with(originator.username) + end + + subject + end + + it 'sends a tracking event to HLLRedisCounter' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter) + .to receive(:track_event).with("i_container_registry_#{event}_#{originator_type}", values: originator.id) + .exactly(count).time + + subject + end + end + context 'with a respository target' do let(:target) do { @@ -164,5 +182,58 @@ RSpec.describe ContainerRegistry::Event do end end end + + context 'with a deploy token as the actor' do + let!(:originator) { create(:deploy_token, username: 'username', id: 3) } + let(:raw_event) do + { + 'action' => 'push', + 'target' => { 'tag' => 'latest' }, + 'actor' => { 'user_type' => 'deploy_token', 'name' => originator.username } + } + end + + it 'does not send a tracking event to HLLRedisCounter' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + subject + end + end + + context 'with a user as the actor' do + let_it_be(:originator) { create(:user, username: 'username') } + let(:raw_event) do + { + 'action' => action, + 'target' => target, + 'actor' => { 'user_type' => user_type, 'name' => originator.username } + } + end + + where(:target, :action, :event, :user_type, :count) do + { 'tag' => 'latest' } | 'push' | 'push_tag' | 'personal_access_token' | 1 + { 'tag' => 'latest' } | 'delete' | 'delete_tag' | 'personal_access_token' | 1 + { 'repository' => 'foo/bar' } | 'push' | 'create_repository' | 'build' | 1 + { 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'gitlab_or_ldap' | 1 + { 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'not_a_user' | 0 + { 'tag' => 'latest' } | 'copy' | '' | nil | 0 + { 'repository' => 'foo/bar' } | 'copy' | '' | '' | 0 + end + + with_them do + it_behaves_like 'tracking event is sent to HLLRedisCounter with event and originator ID', :user + end + end + + context 'without an actor name' do + let(:raw_event) { { 'action' => 'push', 'target' => {}, 'actor' => { 'user_type' => 'personal_access_token' } } } + + it 'does not send a tracking event to HLLRedisCounter' do + expect(User).not_to receive(:find_by_username) + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + subject + end + end end end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index fb6aaffdf22..fe0b46c3117 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ProjectFeature do +RSpec.describe ProjectFeature, feature_category: :projects do using RSpec::Parameterized::TableSyntax let_it_be_with_reload(:project) { create(:project) } @@ -10,6 +10,28 @@ RSpec.describe ProjectFeature do it { is_expected.to belong_to(:project) } + describe 'default values' do + subject { Project.new.project_feature } + + specify { expect(subject.builds_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.issues_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.forking_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.merge_requests_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.snippets_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.wiki_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.repository_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.metrics_dashboard_access_level).to eq(ProjectFeature::PRIVATE) } + specify { expect(subject.operations_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE) } + specify { expect(subject.monitor_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.infrastructure_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.feature_flags_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.environments_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.releases_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.package_registry_access_level).to eq(ProjectFeature::ENABLED) } + specify { expect(subject.container_registry_access_level).to eq(ProjectFeature::ENABLED) } + end + describe 'PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT' do it 'has higher level than that of PRIVATE_FEATURES_MIN_ACCESS_LEVEL' do described_class::PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT.each do |feature, level| diff --git a/spec/services/ci/runners/create_runner_service_spec.rb b/spec/services/ci/runners/create_runner_service_spec.rb new file mode 100644 index 00000000000..673bf3ef90e --- /dev/null +++ b/spec/services/ci/runners/create_runner_service_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ci::Runners::CreateRunnerService, "#execute", feature_category: :runner_fleet do + subject(:execute) { described_class.new(user: current_user, type: type, params: params).execute } + + let(:runner) { execute.payload[:runner] } + + let_it_be(:admin) { create(:admin) } + let_it_be(:non_admin_user) { create(:user) } + let_it_be(:anonymous) { nil } + + shared_context 'when admin user' do + let(:current_user) { admin } + + before do + allow(current_user).to receive(:can?).with(:create_instance_runners).and_return true + end + end + + shared_examples 'it can create a runner' do + it 'creates a runner of the specified type' do + expect(runner.runner_type).to eq expected_type + end + + context 'with default params provided' do + let(:args) do + {} + end + + before do + params.merge!(args) + end + + it { is_expected.to be_success } + + it 'uses default values when none are provided' do + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.persisted?).to be_truthy + expect(runner.run_untagged).to be true + expect(runner.active).to be true + expect(runner.creator).to be current_user + expect(runner.authenticated_user_registration_type?).to be_truthy + expect(runner.runner_type).to eq 'instance_type' + end + end + + context 'with non-default params provided' do + let(:args) do + { + description: 'some description', + maintenance_note: 'a note', + paused: true, + tag_list: %w[tag1 tag2], + access_level: 'ref_protected', + locked: true, + maximum_timeout: 600, + run_untagged: false + } + end + + before do + params.merge!(args) + end + + it { is_expected.to be_success } + + it 'creates runner with specified values', :aggregate_failures do + expect(runner).to be_an_instance_of(::Ci::Runner) + expect(runner.description).to eq 'some description' + expect(runner.maintenance_note).to eq 'a note' + expect(runner.active).to eq !args[:paused] + expect(runner.locked).to eq args[:locked] + expect(runner.run_untagged).to eq args[:run_untagged] + expect(runner.tags).to contain_exactly( + an_object_having_attributes(name: 'tag1'), + an_object_having_attributes(name: 'tag2') + ) + expect(runner.access_level).to eq args[:access_level] + expect(runner.maximum_timeout).to eq args[:maximum_timeout] + + expect(runner.authenticated_user_registration_type?).to be_truthy + expect(runner.runner_type).to eq 'instance_type' + end + end + end + + shared_examples 'it cannot create a runner' do + it 'runner payload is nil' do + expect(runner).to be nil + end + + it { is_expected.to be_error } + end + + shared_examples 'it can return an error' do + let(:group) { create(:group) } + let(:runner_double) { Ci::Runner.new } + + context 'when the runner fails to save' do + before do + allow(Ci::Runner).to receive(:new).and_return runner_double + end + + it_behaves_like 'it cannot create a runner' + + it 'returns error message' do + expect(execute.errors).not_to be_empty + end + end + end + + context 'with type param set to nil' do + let(:expected_type) { 'instance_type' } + let(:type) { nil } + let(:params) { {} } + + it_behaves_like 'it cannot create a runner' do + let(:current_user) { anonymous } + end + + it_behaves_like 'it cannot create a runner' do + let(:current_user) { non_admin_user } + end + + it_behaves_like 'it can create a runner' do + include_context 'when admin user' + end + + it_behaves_like 'it can return an error' do + include_context 'when admin user' + end + end +end |