Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/lint/unused_block_argument.yml1
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml1
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js49
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/bold_text.vue26
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue31
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue34
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/i18n.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/url_sync.vue18
-rw-r--r--app/controllers/dashboard_controller.rb13
-rw-r--r--app/controllers/root_controller.rb4
-rw-r--r--app/helpers/preferences_helper.rb1
-rw-r--r--app/models/container_registry/event.rb41
-rw-r--r--app/models/project_feature.rb65
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/ci/runners/create_runner_service.rb43
-rw-r--r--app/services/ci/runners/runner_creation_strategies/instance_runner_strategy.rb29
-rw-r--r--app/views/dashboard/_activity_head.html.haml3
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml2
-rw-r--r--config/initializers/00_deprecations.rb2
-rw-r--r--config/metrics/counts_28d/20230129134935_i_container_registry_push_tag_user_monthly.yml26
-rw-r--r--config/metrics/counts_28d/20230129135520_i_container_registry_delete_tag_user_monthly.yml26
-rw-r--r--config/metrics/counts_28d/20230129135748_i_container_registry_push_repository_user_monthly.yml26
-rw-r--r--config/metrics/counts_28d/20230129140646_i_container_registry_delete_repository_user_monthly.yml26
-rw-r--r--config/metrics/counts_28d/20230129140954_i_container_registry_create_repository_user_monthly.yml26
-rw-r--r--config/metrics/counts_7d/20230129032841_i_container_registry_push_tag_user_weekly.yml26
-rw-r--r--config/metrics/counts_7d/20230129034513_i_container_registry_delete_tag_user_weekly.yml26
-rw-r--r--config/metrics/counts_7d/20230129034950_i_container_registry_push_repository_user_weekly.yml26
-rw-r--r--config/metrics/counts_7d/20230129035454_i_container_registry_delete_repository_user_weekly.yml26
-rw-r--r--config/metrics/counts_7d/20230129035748_i_container_registry_create_repository_user_weekly.yml26
-rw-r--r--doc/api/draft_notes.md18
-rw-r--r--doc/user/profile/preferences.md16
-rw-r--r--doc/user/project/merge_requests/creating_merge_requests.md13
-rw-r--r--doc/user/project/repository/branches/default.md2
-rw-r--r--doc/user/project/service_desk.md5
-rw-r--r--lib/gitlab/bitbucket_server_import/importer.rb42
-rw-r--r--lib/gitlab/ci/config/external/file/artifact.rb4
-rw-r--r--lib/gitlab/ci/config/external/file/local.rb4
-rw-r--r--lib/gitlab/ci/config/external/file/project.rb9
-rw-r--r--lib/gitlab/pages/cache_control.rb6
-rw-r--r--lib/gitlab/usage_data_counters/known_events/container_registry_events.yml22
-rw-r--r--locale/gitlab.pot184
-rw-r--r--spec/controllers/dashboard_controller_spec.rb47
-rw-r--r--spec/controllers/root_controller_spec.rb22
-rw-r--r--spec/features/dashboard/activity_spec.rb10
-rw-r--r--spec/features/markdown/math_spec.rb77
-rw-r--r--spec/features/merge_request/user_merges_immediately_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_only_if_pipeline_succeeds_spec.rb4
-rw-r--r--spec/features/merge_request/user_resolves_wip_mr_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js19
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js11
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js19
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js11
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js14
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js9
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/url_sync_spec.js58
-rw-r--r--spec/helpers/preferences_helper_spec.rb1
-rw-r--r--spec/lib/gitlab/bitbucket_server_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/file/project_spec.rb18
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb2
-rw-r--r--spec/lib/gitlab/pages/cache_control_spec.rb6
-rw-r--r--spec/models/container_registry/event_spec.rb71
-rw-r--r--spec/models/project_feature_spec.rb24
-rw-r--r--spec/services/ci/runners/create_runner_service_spec.rb135
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
diff --git a/Gemfile b/Gemfile
index 9f398ec2836..b2371f31105 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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: /&amp;lt;script&amp;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