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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-10-19 15:11:29 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-10-19 15:11:29 +0300
commit881435f2a3eeca1b5b544ad7c7510481b1773d1b (patch)
tree34d47e49a899efa730d92d2ea25a31e28be32895
parent91a9a020dafedd084aaa72022f0aa72d14e4f20b (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue63
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue12
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js3
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js86
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue218
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue3
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/build_trace_metadata.rb2
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/concerns/token_authenticatable_strategies/base.rb2
-rw-r--r--app/models/group.rb9
-rw-r--r--app/models/system/broadcast_message.rb2
-rw-r--r--app/models/user.rb6
-rw-r--r--app/services/boards/lists/move_service.rb7
-rw-r--r--app/services/packages/npm/create_package_service.rb8
-rw-r--r--app/services/verify_pages_domain_service.rb2
-rw-r--r--app/views/clusters/clusters/_provider_details_form.html.haml28
-rw-r--r--app/views/groups/_invite_members_modal.html.haml6
-rw-r--r--app/views/projects/_invite_members_modal.html.haml6
-rw-r--r--app/workers/bulk_imports/finish_batched_pipeline_worker.rb5
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb4
-rw-r--r--doc/ci/components/index.md2
-rw-r--r--doc/development/database/iterating_tables_in_batches.md4
-rw-r--r--doc/development/internal_analytics/index.md46
-rw-r--r--doc/development/internal_analytics/service_ping/index.md117
-rw-r--r--doc/development/rubocop_development_guide.md48
-rw-r--r--doc/user/application_security/policies/scan-result-policies.md39
-rw-r--r--gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb2
-rw-r--r--lib/gitlab/auth/two_factor_auth_verifier.rb2
-rw-r--r--lib/gitlab/github_import/client.rb10
-rw-r--r--lib/gitlab/group_search_results.rb2
-rw-r--r--lib/gitlab/import_export/project/sample/date_calculator.rb2
-rw-r--r--locale/gitlab.pot15
-rw-r--r--package.json8
-rw-r--r--spec/features/projects/issues/email_participants_spec.rb7
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js60
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js15
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js13
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js323
-rw-r--r--spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js1
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js12
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb39
-rw-r--r--spec/models/ci/build_spec.rb2
-rw-r--r--spec/models/group_spec.rb23
-rw-r--r--spec/services/packages/npm/create_package_service_spec.rb78
-rw-r--r--spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb29
-rw-r--r--spec/workers/bulk_imports/pipeline_worker_spec.rb30
-rw-r--r--vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock141
-rw-r--r--vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec4
53 files changed, 1220 insertions, 358 deletions
diff --git a/Gemfile.lock b/Gemfile.lock
index 052a59d6b7f..c5d2d2b1634 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -103,7 +103,7 @@ PATH
specs:
devise-pbkdf2-encryptable (0.0.0)
devise (~> 4.0)
- devise-two-factor (~> 4.0)
+ devise-two-factor (~> 4.1.1)
PATH
remote: vendor/gems/mail-smtp_pool
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index 509efd31dcd..505612c59da 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,12 +1,12 @@
<script>
-import { GlAlert, GlButton, GlCollapse, GlIcon } from '@gitlab/ui';
+import { GlAlert, GlButton, GlCollapse, GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
import { partition, isString, uniqueId, isEmpty } from 'lodash';
import SafeHtml from '~/vue_shared/directives/safe_html';
import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_base.vue';
import Api from '~/api';
import Tracking from '~/tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
-import { n__, sprintf } from '~/locale';
+import { n__, s__, sprintf } from '~/locale';
import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
import { captureException } from '~/ci/runner/sentry_utils';
import {
@@ -31,7 +31,9 @@ export default {
GlAlert,
GlButton,
GlCollapse,
+ GlLink,
GlIcon,
+ GlSprintf,
InviteModalBase,
MembersTokenSelect,
ModalConfetti,
@@ -43,6 +45,17 @@ export default {
SafeHtml,
},
mixins: [Tracking.mixin({ category: INVITE_MEMBER_MODAL_TRACKING_CATEGORY })],
+ inject: {
+ isCurrentUserAdmin: {
+ default: false,
+ },
+ isEmailSignupEnabled: {
+ default: true,
+ },
+ newUsersUrl: {
+ default: '',
+ },
+ },
props: {
id: {
type: String,
@@ -122,6 +135,9 @@ export default {
isCelebration() {
return this.mode === 'celebrate';
},
+ isTextForAdmin() {
+ return this.isCurrentUserAdmin && Boolean(this.newUsersUrl);
+ },
modalTitle() {
return this.$options.labels.modal[this.mode].title;
},
@@ -131,6 +147,11 @@ export default {
labelIntroText() {
return this.$options.labels[this.inviteTo][this.mode].introText;
},
+ labelSearchField() {
+ return this.isEmailSignupEnabled
+ ? this.$options.labels.searchField
+ : s__('InviteMembersModal|Username');
+ },
isEmptyInvites() {
return Boolean(this.newUsersToInvite.length);
},
@@ -144,6 +165,14 @@ export default {
this.errorList.length,
);
},
+ signupDisabledText() {
+ return s__(
+ "InviteMembersModal|Administrators can %{linkStart}add new users by email manually%{linkEnd}. After they've been added, you can invite them to this group with their username.",
+ );
+ },
+ signupDisabledTitle() {
+ return s__('InviteMembersModal|Inviting users by email is disabled');
+ },
showUserLimitNotification() {
return !isEmpty(this.usersLimitDataset.alertVariant);
},
@@ -173,8 +202,13 @@ export default {
count: this.errorsExpanded.length,
});
},
+ formGroupDescriptionText() {
+ return this.isEmailSignupEnabled
+ ? this.$options.labels.placeHolder
+ : s__('InviteMembersModal|Select members');
+ },
formGroupDescription() {
- return this.invalidFeedbackMessage ? null : this.$options.labels.placeHolder;
+ return this.invalidFeedbackMessage ? null : this.formGroupDescriptionText;
},
},
watch: {
@@ -224,7 +258,7 @@ export default {
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
showEmptyInvitesAlert() {
- this.invalidFeedbackMessage = this.$options.labels.placeHolder;
+ this.invalidFeedbackMessage = this.formGroupDescriptionText;
this.shouldShowEmptyInvitesAlert = true;
this.$refs.alerts.focus();
},
@@ -345,7 +379,7 @@ export default {
:default-access-level="defaultAccessLevel"
:help-link="helpLink"
:label-intro-text="labelIntroText"
- :label-search-field="$options.labels.searchField"
+ :label-search-field="labelSearchField"
:form-group-description="formGroupDescription"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
@@ -429,6 +463,24 @@ export default {
</gl-button>
</template>
</gl-alert>
+ <gl-alert
+ v-if="!isEmailSignupEnabled"
+ id="signup-disabled-alert"
+ :dismissible="false"
+ :title="signupDisabledTitle"
+ class="gl-mb-4"
+ variant="warning"
+ data-testid="email-signup-disabled-alert"
+ >
+ <gl-sprintf :message="signupDisabledText">
+ <template #link="{ content }">
+ <gl-link v-if="isTextForAdmin" :href="newUsersUrl" target="_blank">{{
+ content
+ }}</gl-link>
+ <span v-else>{{ content }}</span>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<user-limit-notification
v-else-if="showUserLimitNotification"
class="gl-mb-5"
@@ -447,6 +499,7 @@ export default {
v-model="newUsersToInvite"
class="gl-mb-2"
aria-labelledby="empty-invites-alert"
+ :can-use-email-token="isEmailSignupEnabled"
:input-id="inputId"
:exception-state="exceptionState"
:users-filter="usersFilter"
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 8493787f075..0be04b7af35 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -21,6 +21,11 @@ export default {
GlSprintf,
},
props: {
+ canUseEmailToken: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
placeholder: {
type: String,
required: false,
@@ -68,6 +73,10 @@ export default {
},
computed: {
emailIsValid() {
+ if (!this.canUseEmailToken) {
+ return false;
+ }
+
const regex = /^\S+@\S+$/;
return this.originalInput.match(regex) !== null;
@@ -137,9 +146,8 @@ export default {
username: token.username,
avatar_url: token.avatar_url,
}));
- this.loading = false;
})
- .catch(() => {
+ .finally(() => {
this.loading = false;
});
}, SEARCH_DELAY),
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index 41ed0179364..8dfe697e2cb 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -25,6 +25,9 @@ export default (function initInviteMembersModal() {
name: 'InviteMembersModalRoot',
provide: {
name: el.dataset.name,
+ newUsersUrl: el.dataset.newUsersUrl,
+ isCurrentUserAdmin: parseBoolean(el.dataset.isCurrentUserAdmin),
+ isEmailSignupEnabled: parseBoolean(el.dataset.isSignupEnabled),
},
render: (createElement) =>
createElement(InviteMembersModal, {
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 5416f86abeb..ff48b8d92cc 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -70,14 +70,16 @@ export default {
return {
isMouseIn: false,
canClickPinButton: false,
- pillCount: this.item.pill_count,
};
},
computed: {
+ pillData() {
+ return this.item.pill_count;
+ },
hasPill() {
return (
- Number.isFinite(this.pillCount) ||
- (typeof this.pillCount === 'string' && this.pillCount !== '')
+ Number.isFinite(this.pillData) ||
+ (typeof this.pillData === 'string' && this.pillData !== '')
);
},
isPinnable() {
@@ -193,7 +195,11 @@ export default {
},
updatePillValue({ value, itemId }) {
if (this.item.id === itemId) {
- this.pillCount = value;
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/428246
+ // fixing this linting issue is causing the pills not to async update
+ //
+ // eslint-disable-next-line vue/no-mutating-props
+ this.item.pill_count = value;
}
},
},
@@ -258,7 +264,7 @@ export default {
'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable,
}"
>
- {{ pillCount }}
+ {{ pillData }}
</gl-badge>
</span>
</component>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
new file mode 100644
index 00000000000..7458a2503e8
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
@@ -0,0 +1,86 @@
+import createMockApollo from 'helpers/mock_apollo_helper';
+import rebaseStateQuery from '../../queries/states/rebase.query.graphql';
+import Rebase from './rebase.vue';
+
+const service = {
+ rebase: () => new Promise(() => {}),
+};
+
+const defaultRender = ({ apolloProvider, check, mr, canCreatePipelineInTargetProject }) => ({
+ components: { Rebase },
+ apolloProvider,
+ provide: {
+ canCreatePipelineInTargetProject,
+ },
+ data() {
+ return { service, mr: { ...mr, targetProjectFullPath: 'gitlab-org/gitlab' }, check };
+ },
+ template: '<rebase :mr="mr" :service="service" :check="check" />',
+});
+
+const Template = ({
+ failed,
+ pushToSourceBranch,
+ rebaseInProgress,
+ onlyAllowMergeIfPipelineSucceeds,
+ canCreatePipelineInTargetProject,
+}) => {
+ const requestHandlers = [
+ [
+ rebaseStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress,
+ targetBranch: 'main',
+ userPermissions: {
+ pushToSourceBranch,
+ },
+ pipelines: {
+ nodes: [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'gitlab/gitlab',
+ },
+ },
+ ],
+ },
+ },
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender({
+ apolloProvider,
+ check: {
+ failureReason: 'Needs rebasing',
+ identifier: 'rebase',
+ result: failed ? 'failed' : 'passed',
+ },
+ mr: { onlyAllowMergeIfPipelineSucceeds },
+ canCreatePipelineInTargetProject,
+ });
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ failed: true,
+ pushToSourceBranch: true,
+ rebaseInProgress: false,
+ onlyAllowMergeIfPipelineSucceeds: false,
+ canCreatePipelineInTargetProject: false,
+};
+
+export default {
+ title: 'vue_merge_request_widget/merge_checks/rebase',
+ component: Rebase,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
new file mode 100644
index 00000000000..823a30c7063
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
@@ -0,0 +1,218 @@
+<script>
+import { GlModal, GlLink } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { createAlert } from '~/alert';
+import toast from '~/vue_shared/plugins/global_toast';
+import simplePoll from '~/lib/utils/simple_poll';
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+import rebaseQuery from '../../queries/states/rebase.query.graphql';
+import eventHub from '../../event_hub';
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+export default {
+ name: 'MergeChecksRebase',
+ components: {
+ GlModal,
+ GlLink,
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ apollo: {
+ state: {
+ query: rebaseQuery,
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data.project.mergeRequest,
+ },
+ },
+ inject: {
+ canCreatePipelineInTargetProject: {
+ default: false,
+ },
+ },
+ props: {
+ check: {
+ type: Object,
+ required: true,
+ },
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ service: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ state: {},
+ isMakingRequest: false,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ rebaseInProgress() {
+ return this.state.rebaseInProgress;
+ },
+ showRebaseWithoutPipeline() {
+ return (
+ !this.mr.onlyAllowMergeIfPipelineSucceeds ||
+ (this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.allowMergeOnSkippedPipeline)
+ );
+ },
+ isForkMergeRequest() {
+ return this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath;
+ },
+ isLatestPipelineCreatedInTargetProject() {
+ const latestPipeline = this.state.pipelines.nodes[0];
+
+ return latestPipeline?.project?.fullPath === this.mr.targetProjectFullPath;
+ },
+ shouldShowSecurityWarning() {
+ return (
+ this.canCreatePipelineInTargetProject &&
+ this.isForkMergeRequest &&
+ !this.isLatestPipelineCreatedInTargetProject
+ );
+ },
+ tertiaryActionsButtons() {
+ if (this.check.result === 'success') return [];
+
+ return [
+ {
+ text: s__('mrWidget|Rebase'),
+ loading: this.isMakingRequest || this.rebaseInProgress,
+ testId: 'standard-rebase-button',
+ onClick: () => this.tryRebase(),
+ },
+ this.showRebaseWithoutPipeline && {
+ text: s__('mrWidget|Rebase without pipeline'),
+ loading: this.isMakingRequest || this.rebaseInProgress,
+ testId: 'rebase-without-ci-button',
+ onClick: () => this.rebaseWithoutCi(),
+ },
+ ].filter((b) => b);
+ },
+ },
+ methods: {
+ rebase({ skipCi = false } = {}) {
+ this.isMakingRequest = true;
+
+ this.service
+ .rebase({ skipCi })
+ .then(() => simplePoll(this.checkRebaseStatus))
+ .catch((error) => {
+ this.isMakingRequest = false;
+
+ if (!error.response?.data?.merge_error) {
+ createAlert({
+ message: __('Something went wrong. Please try again.'),
+ });
+ }
+ });
+ },
+ rebaseWithoutCi() {
+ return this.rebase({ skipCi: true });
+ },
+ tryRebase() {
+ if (this.shouldShowSecurityWarning) {
+ this.$refs.modal.show();
+ } else {
+ this.rebase();
+ }
+ },
+ checkRebaseStatus(continuePolling, stopPolling) {
+ this.service
+ .poll()
+ .then((res) => res.data)
+ .then((res) => {
+ if (res.rebase_in_progress || res.should_be_rebased) {
+ continuePolling();
+ } else {
+ this.isMakingRequest = false;
+
+ if (!res.merge_error?.length) {
+ toast(__('Rebase completed'));
+ }
+
+ eventHub.$emit('MRWidgetRebaseSuccess');
+ stopPolling();
+ }
+ })
+ .catch(() => {
+ this.isMakingRequest = false;
+ createAlert({
+ message: __('Something went wrong. Please try again.'),
+ });
+ stopPolling();
+ });
+ },
+ },
+ modal: {
+ id: 'rebase-security-risk-modal',
+ title: s__('mrWidget|Are you sure you want to rebase?'),
+ actionPrimary: {
+ text: s__('mrWidget|Rebase'),
+ attributes: {
+ variant: 'danger',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
+ runPipelinesInTheParentProjectHelpPath: helpPagePath(
+ '/ci/pipelines/merge_request_pipelines.html',
+ {
+ anchor: 'run-pipelines-in-the-parent-project',
+ },
+ ),
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <action-buttons v-if="!isLoading" :tertiary-buttons="tertiaryActionsButtons" />
+ <gl-modal
+ ref="modal"
+ :modal-id="$options.modal.id"
+ :title="$options.modal.title"
+ :action-primary="$options.modal.actionPrimary"
+ :action-cancel="$options.modal.actionCancel"
+ @primary="rebase"
+ >
+ <p>
+ {{
+ s__(
+ 'Pipelines|Rebasing creates a pipeline that runs code originating from a forked project merge request. Consequently there are potential security implications, such as the exposure of CI variables.',
+ )
+ }}
+ </p>
+ <p>
+ {{
+ s__(
+ "Pipelines|You should review the code thoroughly before running this pipeline with the parent project's CI/CD resources.",
+ )
+ }}
+ </p>
+ <p>
+ {{ s__('Pipelines|If you are unsure, ask a project maintainer to review it for you.') }}
+ </p>
+ <gl-link :href="$options.runPipelinesInTheParentProjectHelpPath" target="_blank">
+ {{ s__('Pipelines|More Information') }}
+ </gl-link>
+ </gl-modal>
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
index 1c57226f887..a9745f3214c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
@@ -15,7 +15,7 @@ const defaultRender = (apolloProvider) => ({
components: { MergeChecks },
apolloProvider,
data() {
- return { mr: { conflictResolutionPath: 'https://gitlab.com' } };
+ return { service: {}, mr: { conflictResolutionPath: 'https://gitlab.com' } };
},
template: '<merge-checks :mr="mr" />',
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
index fa84c0a4a6f..5652b81386f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
@@ -8,6 +8,7 @@ import BoldText from './bold_text.vue';
const COMPONENTS = {
conflicts: () => import('./checks/conflicts.vue'),
+ rebase: () => import('./checks/rebase.vue'),
default: () => import('./checks/message.vue'),
};
@@ -35,6 +36,10 @@ export default {
type: Object,
required: true,
},
+ service: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -122,6 +127,7 @@ export default {
}"
:check="check"
:mr="mr"
+ :service="service"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index ac434c5be4e..ac7e44364d8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -730,6 +730,9 @@ export default {
class="mr-ready-merge-related-links gl-display-inline"
/>
</li>
+ <li v-if="state.autoMergeEnabled" class="gl-line-height-normal">
+ {{ s__('mrWidget|Auto-merge enabled') }}
+ </li>
</ul>
</div>
</div>
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d2cf9058976..a4724fd7c02 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -729,7 +729,7 @@ module Ci
end
def artifacts_expired?
- artifacts_expire_at && artifacts_expire_at < Time.current
+ artifacts_expire_at&.past?
end
def artifacts_expire_in
@@ -745,7 +745,7 @@ module Ci
def has_expired_locked_archive_artifacts?
locked_artifacts? &&
- artifacts_expire_at.present? && artifacts_expire_at < Time.current
+ artifacts_expire_at&.past?
end
def has_expiring_archive_artifacts?
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
index c5ad3d19425..525cb08f2ca 100644
--- a/app/models/ci/build_trace_metadata.rb
+++ b/app/models/ci/build_trace_metadata.rb
@@ -33,7 +33,7 @@ module Ci
return false unless archival_attempts_available?
return true unless last_archival_attempt_at
- last_archival_attempt_at + backoff < Time.current
+ (last_archival_attempt_at + backoff).past?
end
def archival_attempts_available?
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 2a346f97958..fe4437a4ad6 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -306,7 +306,7 @@ module Ci
end
def expired?
- expire_at.present? && expire_at < Time.current
+ expire_at.present? && expire_at.past?
end
def expiring?
diff --git a/app/models/concerns/token_authenticatable_strategies/base.rb b/app/models/concerns/token_authenticatable_strategies/base.rb
index d0085b60d98..b25ee434484 100644
--- a/app/models/concerns/token_authenticatable_strategies/base.rb
+++ b/app/models/concerns/token_authenticatable_strategies/base.rb
@@ -65,7 +65,7 @@ module TokenAuthenticatableStrategies
return false unless expirable? && token_expiration_enforced?
exp = expires_at(instance)
- !!exp && Time.current > exp
+ !!exp && exp.past?
end
def expirable?
diff --git a/app/models/group.rb b/app/models/group.rb
index c83dd24e98e..919b80ccffb 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -671,15 +671,6 @@ class Group < Namespace
members.count
end
- # Returns all users that are members of projects
- # belonging to the current group or sub-groups
- def project_users_with_descendants
- User
- .joins(projects: :group)
- .where(namespaces: { id: self_and_descendants.select(:id) })
- .allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
- end
-
# Return the highest access level for a user
#
# A special case is handled here when the user is a GitLab admin
diff --git a/app/models/system/broadcast_message.rb b/app/models/system/broadcast_message.rb
index 06f0115ade6..d959a6339a4 100644
--- a/app/models/system/broadcast_message.rb
+++ b/app/models/system/broadcast_message.rb
@@ -117,7 +117,7 @@ module System
end
def ended?
- ends_at < Time.current
+ ends_at.past?
end
def now?
diff --git a/app/models/user.rb b/app/models/user.rb
index 4034677509f..5b6d9f3b6e8 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1081,7 +1081,7 @@ class User < MainClusterwide::ApplicationRecord
def otp_secret_expired?
return true unless otp_secret_expires_at
- otp_secret_expires_at < Time.current
+ otp_secret_expires_at.past?
end
def update_otp_secret!
@@ -1446,7 +1446,7 @@ class User < MainClusterwide::ApplicationRecord
if !Gitlab.config.ldap.enabled
false
elsif ldap_user?
- !last_credential_check_at || (last_credential_check_at + ldap_sync_time) < Time.current
+ !last_credential_check_at || (last_credential_check_at + ldap_sync_time).past?
else
false
end
@@ -2087,7 +2087,7 @@ class User < MainClusterwide::ApplicationRecord
end
def password_expired?
- !!(password_expires_at && password_expires_at < Time.current)
+ !!(password_expires_at && password_expires_at.past?)
end
def password_expired_if_applicable?
diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb
index 4bb7b4dbc6d..4715f1276e3 100644
--- a/app/services/boards/lists/move_service.rb
+++ b/app/services/boards/lists/move_service.rb
@@ -22,8 +22,11 @@ module Boards
attr_reader :board, :old_position, :new_position
def valid_move?
- new_position.present? && new_position != old_position &&
- new_position >= 0 && new_position <= board.lists.movable.last.position
+ new_position.present? && new_position != old_position && new_position.between?(0, max_position)
+ end
+
+ def max_position
+ board.lists.movable.maximum(:position)
end
def reorder_intermediate_lists
diff --git a/app/services/packages/npm/create_package_service.rb b/app/services/packages/npm/create_package_service.rb
index d599cecc8da..0f0dc297e9a 100644
--- a/app/services/packages/npm/create_package_service.rb
+++ b/app/services/packages/npm/create_package_service.rb
@@ -12,6 +12,7 @@ module Packages
return error('Version is empty.', 400) if version.blank?
return error('Attachment data is empty.', 400) if attachment['data'].blank?
return error('Package already exists.', 403) if current_package_exists?
+ return error('Package protected.', 403) if current_package_protected?
return error('File is too large.', 400) if file_size_exceeded?
package = try_obtain_lease do
@@ -56,6 +57,13 @@ module Packages
.exists?
end
+ def current_package_protected?
+ return false if Feature.disabled?(:packages_protected_packages, project)
+
+ user_project_authorization_access_level = current_user.max_member_access_for_project(project.id)
+ project.package_protection_rules.push_protected_from?(access_level: user_project_authorization_access_level, package_name: name, package_type: :npm)
+ end
+
def name
params[:name]
end
diff --git a/app/services/verify_pages_domain_service.rb b/app/services/verify_pages_domain_service.rb
index 59c73aa929c..f5dfe13539b 100644
--- a/app/services/verify_pages_domain_service.rb
+++ b/app/services/verify_pages_domain_service.rb
@@ -79,7 +79,7 @@ class VerifyPagesDomainService < BaseService
# A domain is only expired until `disable!` has been called
def expired?
- domain.enabled_until && domain.enabled_until < Time.current
+ domain.enabled_until&.past?
end
def dns_record_present?
diff --git a/app/views/clusters/clusters/_provider_details_form.html.haml b/app/views/clusters/clusters/_provider_details_form.html.haml
index 4b7164f9845..dfb97263c54 100644
--- a/app/views/clusters/clusters/_provider_details_form.html.haml
+++ b/app/views/clusters/clusters/_provider_details_form.html.haml
@@ -1,35 +1,35 @@
= gitlab_ui_form_for cluster, url: update_cluster_url_path, html: { class: 'js-provider-details gl-show-field-errors', role: 'form' },
as: :cluster do |field|
.form-group
- - copy_name_btn = deprecated_clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold required'
.input-group.gl-field-error-anchor
= field.text_field :name, class: 'form-control js-select-on-focus cluster-name', required: true,
title: s_('ClusterIntegration|Cluster name is required.'),
- readonly: cluster.read_only_kubernetes_platform_fields?,
- append: copy_name_btn
+ readonly: cluster.read_only_kubernetes_platform_fields?
+ - if cluster.read_only_kubernetes_platform_fields?
+ .input-group-append
+ = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), variant: :default, category: :primary, size: :medium)
= field.fields_for :platform_kubernetes, platform do |platform_field|
.form-group
- - copy_api_url = deprecated_clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold required'
.input-group.gl-field-error-anchor
= platform_field.text_field :api_url, class: 'form-control js-select-on-focus', required: true,
title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
- readonly: cluster.read_only_kubernetes_platform_fields?,
- append: copy_api_url
+ readonly: cluster.read_only_kubernetes_platform_fields?
+ - if cluster.read_only_kubernetes_platform_fields?
+ .input-group-append
+ = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), variant: :default, category: :primary, size: :medium)
.form-group
- - copy_ca_cert_btn = deprecated_clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
- class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
= platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
- .input-group.gl-field-error-anchor
- = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', rows: '10',
+ .input-group.gl-field-error-anchor.markdown-code-block
+ = platform_field.text_area :ca_cert, class: 'gl-rounded-top-right-base! gl-rounded-bottom-right-base! form-control js-select-on-focus', rows: '10',
readonly: cluster.read_only_kubernetes_platform_fields?,
- placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
- append: copy_ca_cert_btn
+ placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
+ - if cluster.read_only_kubernetes_platform_fields?
+ %copy-code
+ = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), variant: :default, category: :primary, size: :medium, class: 'copy-code')
.form-group
= platform_field.label :token, s_('ClusterIntegration|Enter new Service Token'), class: 'label-bold required'
diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml
index cd3327ba9ec..d53190948fd 100644
--- a/app/views/groups/_invite_members_modal.html.haml
+++ b/app/views/groups/_invite_members_modal.html.haml
@@ -3,4 +3,8 @@
.js-invite-members-modal{ data: { is_project: 'false',
access_levels: group.access_level_roles.to_json,
reload_page_on_submit: current_path?('group_members#index').to_s,
- help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }
+ help_link: help_page_url('user/permissions'),
+ is_signup_enabled: signup_enabled?.to_s,
+ new_users_url: new_admin_user_url,
+ is_current_user_admin: current_user&.admin?.to_s,
+ }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }
diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml
index a1b0bdd6c56..8713cb4990a 100644
--- a/app/views/projects/_invite_members_modal.html.haml
+++ b/app/views/projects/_invite_members_modal.html.haml
@@ -3,4 +3,8 @@
.js-invite-members-modal{ data: { is_project: 'true',
access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json,
reload_page_on_submit: current_path?('project_members#index').to_s,
- help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
+ help_link: help_page_url('user/permissions'),
+ is_signup_enabled: signup_enabled?.to_s,
+ new_users_url: new_admin_user_url,
+ is_current_user_admin: current_user&.admin?.to_s,
+ }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
diff --git a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
index b1f3757e058..b953f8ab786 100644
--- a/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
+++ b/app/workers/bulk_imports/finish_batched_pipeline_worker.rb
@@ -27,11 +27,6 @@ module BulkImports
else
tracker.finish!
end
-
- ensure
- # This is needed for in-flight migrations.
- # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299
- ::BulkImports::EntityWorker.perform_async(tracker.entity.id) if job_version.nil?
end
private
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index 24185f43795..2f57c4579cc 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -34,10 +34,6 @@ module BulkImports
fail_tracker(StandardError.new(message)) unless pipeline_tracker.finished? || pipeline_tracker.skipped?
end
end
- ensure
- # This is needed for in-flight migrations.
- # It will be remove in https://gitlab.com/gitlab-org/gitlab/-/issues/426299
- ::BulkImports::EntityWorker.perform_async(entity_id) if job_version.nil?
end
private
diff --git a/doc/ci/components/index.md b/doc/ci/components/index.md
index a3d6d7224e4..8246e1c4073 100644
--- a/doc/ci/components/index.md
+++ b/doc/ci/components/index.md
@@ -395,7 +395,7 @@ For example:
```yaml
include:
# include the component located in the current project from the current SHA
- - component: gitlab.com/$CI_PROJECT_PATH@$CI_COMMIT_SHA
+ - component: gitlab.com/$CI_PROJECT_PATH/my-component@$CI_COMMIT_SHA
inputs:
stage: build
diff --git a/doc/development/database/iterating_tables_in_batches.md b/doc/development/database/iterating_tables_in_batches.md
index 84b82b16255..44a8c72ea2c 100644
--- a/doc/development/database/iterating_tables_in_batches.md
+++ b/doc/development/database/iterating_tables_in_batches.md
@@ -523,14 +523,14 @@ and resumed at any point. This capability is demonstrated in the following code
stop_at = Time.current + 3.minutes
count, last_value = Issue.each_batch_count do
- Time.current > stop_at # condition for stopping the counting
+ stop_at.past? # condition for stopping the counting
end
# Continue the counting later
stop_at = Time.current + 3.minutes
count, last_value = Issue.each_batch_count(last_count: count, last_value: last_value) do
- Time.current > stop_at
+ stop_at.past?
end
```
diff --git a/doc/development/internal_analytics/index.md b/doc/development/internal_analytics/index.md
index 64b9c7af037..d02e366252a 100644
--- a/doc/development/internal_analytics/index.md
+++ b/doc/development/internal_analytics/index.md
@@ -50,9 +50,53 @@ such as the value of a setting or the count of rows in a database table.
- To instrument an event-based metric, see the [internal event tracking quick start guide](internal_event_instrumentation/quick_start.md).
- To instrument a metric that observes the GitLab instances state, see [the metrics instrumentation](metrics/metrics_instrumentation.md).
-## Data flow
+## Data availability
For GitLab there is an essential difference in analytics setup between SaaS and self-managed or GitLab Dedicated instances.
+On our SaaS instance both individual events and pre-computed metrics are available for analysis.
+Additionally for SaaS page views are automatically instrumented.
+For self-managed only the metrics instrumenented on the version installed on the instance are available.
+
+## Data discovery
+
+The data visualization tools [Sisense](https://about.gitlab.com/handbook/business-technology/data-team/platform/sisensecdt/) and [Tableau](https://about.gitlab.com/handbook/business-technology/data-team/platform/tableau/),
+which have access to our Data Warehouse, can be used to query the internal analytics data.
+
+### Querying metrics
+
+The following example query returns all values reported for `count_distinct_user_id_from_feature_used_7d` within the last six months and the according `instance_id`:
+
+```sql
+SELECT
+ date_trunc('week', ping_created_at),
+ dim_instance_id,
+ metric_value
+FROM common.fct_ping_instance_metric_rolling_6_months --model limited to last 6 months for performance
+WHERE metrics_path = 'counts.users_visiting_dashboard_weekly' --set to metric of interest
+ORDER BY ping_created_at DESC
+```
+
+For a list of other metrics tables refer to the [Data Models Cheat Sheet](https://about.gitlab.com/handbook/product/product-analysis/data-model-cheat-sheet/#commonly-used-data-models).
+
+### Querying events
+
+The following example query returns the number of daily event occurences for the `feature_used` event.
+
+```sql
+SELECT
+ behavior_date,
+ COUNT(*) as event_occurences
+FROM common_mart.mart_behavior_structured_event
+WHERE event_action = 'feature_used'
+AND event_category = 'InternalEventTracking'
+AND behavior_date > '2023-08-01' --restricted minimum date for performance
+GROUP BY 1 ORDER BY 1 desc
+```
+
+For a list of other event tables refer to the [Data Models Cheat Sheet](https://about.gitlab.com/handbook/product/product-analysis/data-model-cheat-sheet/#commonly-used-data-models-2).
+
+## Data flow
+
On SaaS event records are directly sent to a collection system, called Snowplow, and imported into our data warehouse.
Self-managed and GitLab Dedicated instances record event counts locally. Every week, a process called Service Ping sends the current
values for all pre-defined and active metrics to our data warehouse. For GitLab.com, metrics are calculated directly in the data warehouse.
diff --git a/doc/development/internal_analytics/service_ping/index.md b/doc/development/internal_analytics/service_ping/index.md
index bae4e35149d..08e669ab413 100644
--- a/doc/development/internal_analytics/service_ping/index.md
+++ b/doc/development/internal_analytics/service_ping/index.md
@@ -38,13 +38,8 @@ We use the following terminology to describe the Service Ping components:
### Limitations
-- Service Ping does not track frontend events things like page views, link clicks, or user sessions.
-- Service Ping focuses only on aggregated backend events.
-
-Because of these limitations we recommend you:
-
-- Instrument your products with Snowplow for more detailed analytics on GitLab.com.
-- Use Service Ping to track aggregated backend events on self-managed instances.
+- Service Ping delivers only [metrics](../index.md#metric), not individual events.
+- A metric has to be present and instrumented in the codebase for a GitLab version to be delivered in Service Pings for that version.
## Service Ping request flow
@@ -358,14 +353,6 @@ The following is example content of the Service Ping payload.
}
```
-## Notable changes
-
-In GitLab 14.6, [`flavor`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/75587) was added to try to detect the underlying managed database variant.
-Possible values are "Amazon Aurora PostgreSQL", "PostgreSQL on Amazon RDS", "Cloud SQL for PostgreSQL",
-"Azure Database for PostgreSQL - Flexible Server", or "null".
-
-In GitLab 13.5, `pg_system_id` was added to send the [PostgreSQL system identifier](https://www.2ndquadrant.com/en/blog/support-for-postgresqls-system-identifier-in-barman/).
-
## Export Service Ping data
Rake tasks exist to export Service Ping data in different formats.
@@ -390,105 +377,7 @@ bin/rake gitlab:usage_data:dump_non_sql_in_json
bin/rake gitlab:usage_data:dump_sql_in_yaml > ~/Desktop/usage-metrics-2020-09-02.yaml
```
-## Generate Service Ping
-
-To generate Service Ping, use [Teleport](https://goteleport.com/docs/) or a detached screen session on a remote server.
-
-### Triggering
-
-#### Trigger Service Ping with Teleport
-
-1. Request temporary [access](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/teleport/Connect_to_Rails_Console_via_Teleport.md#how-to-use-teleport-to-connect-to-rails-console) to the required environment.
-1. After your approval is issued, [access the Rails console](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/teleport/Connect_to_Rails_Console_via_Teleport.md#access-approval).
-1. Run `GitlabServicePingWorker.new.perform('triggered_from_cron' => false)`.
-
-#### Trigger Service Ping with a detached screen session
-
-1. Connect to bastion with agent forwarding:
-
- ```shell
- ssh -A lb-bastion.gprd.gitlab.com
- ```
-
-1. Create named screen:
-
- ```shell
- screen -S <username>_usage_ping_<date>
- ```
-
-1. Connect to console host:
-
- ```shell
- ssh $USER-rails@console-01-sv-gprd.c.gitlab-production.internal
- ```
-
-1. Run:
-
- ```shell
- GitlabServicePingWorker.new.perform('triggered_from_cron' => false)
- ```
-
-1. To detach from screen, press `ctrl + A`, `ctrl + D`.
-1. Exit from bastion:
-
- ```shell
- exit
- ```
-
-1. Get the metrics duration from logs:
-
-Search in Google Console logs for `time_elapsed`. [Query example](https://cloudlogging.app.goo.gl/nWheZvD8D3nWazNe6).
-
-### Verification (After approx 30 hours)
-
-#### Verify with Teleport
-
-1. Follow [the steps](https://gitlab.com/gitlab-com/runbooks/-/blob/master/docs/teleport/Connect_to_Rails_Console_via_Teleport.md#how-to-use-teleport-to-connect-to-rails-console) to request a new access to the required environment and connect to the Rails console
-1. Check the last payload in `raw_usage_data` table: `RawUsageData.last.payload`
-1. Check the when the payload was sent: `RawUsageData.last.sent_at`
-
-#### Verify using detached screen session
-
-1. Reconnect to bastion:
-
- ```shell
- ssh -A lb-bastion.gprd.gitlab.com
- ```
-
-1. Find your screen session:
-
- ```shell
- screen -ls
- ```
-
-1. Attach to your screen session:
-
- ```shell
- screen -x 14226.mwawrzyniak_usage_ping_2021_01_22
- ```
-
-1. Check the last payload in `raw_usage_data` table:
-
- ```shell
- RawUsageData.last.payload
- ```
-
-1. Check the when the payload was sent:
-
- ```shell
- RawUsageData.last.sent_at
- ```
-
-### Skip database write operations
-
-To skip database write operations, DevOps report creation, and storage of usage data payload, pass an optional argument:
-
-```shell
-skip_db_write:
-GitlabServicePingWorker.new.perform('triggered_from_cron' => false, 'skip_db_write' => true)
-```
-
-### Fallback values for Service Ping
+## Fallback values for Service Ping
We return fallback values in these cases:
diff --git a/doc/development/rubocop_development_guide.md b/doc/development/rubocop_development_guide.md
index 6568d025ca5..807544b71d4 100644
--- a/doc/development/rubocop_development_guide.md
+++ b/doc/development/rubocop_development_guide.md
@@ -28,15 +28,51 @@ discussions, nitpicking, or back-and-forth in reviews. The
[GitLab Ruby style guide](backend/ruby_style_guide.md) includes a non-exhaustive
list of styles that commonly come up in reviews and are not enforced.
-By default, we should not
-[disable a RuboCop rule inline](https://docs.rubocop.org/rubocop/configuration.html#disabling-cops-within-source-code), because it negates agreed-upon code standards that the rule is attempting to apply to the codebase.
-
-If you must use inline disable, provide the reason on the MR and ensure the reviewers agree
-before merging.
-
Additionally, we have dedicated
[test-specific style guides and best practices](testing_guide/index.md).
+## Disabling rules inline
+
+By default, RuboCop rules should not be
+[disabled inline](https://docs.rubocop.org/rubocop/configuration.html#disabling-cops-within-source-code),
+because it negates agreed-upon code standards that the rule is attempting to
+apply to the codebase.
+
+If you must use inline disable provide the reason as a code comment in
+the same line where the rule is disabled.
+
+More context can go into code comments above this inline disable comment. To
+reduce verbose code comments link a resource (issue, epic, ...) to provide
+detailed context.
+
+For example:
+
+```ruby
+# bad
+module Types
+ module Domain
+ # rubocop:disable Graphql/AuthorizeTypes
+ class SomeType < BaseObject
+ object.public_send(action) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ # rubocop:enable Graphql/AuthorizeTypes
+ end
+end
+
+# good
+module Types
+ module Domain
+ # rubocop:disable Graphql/AuthorizeTypes -- already authroized in parent entity
+ class SomeType < BaseObject
+ # At this point `action` is safe to be used in `public_send`.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/123457890.
+ object.public_send(action) # rubocop:disable GitlabSecurity/PublicSend -- User input verified
+ end
+ # rubocop:enable Graphql/AuthorizeTypes
+ end
+end
+```
+
## Creating new RuboCop cops
Typically it is better for the linting rules to be enforced programmatically as it
diff --git a/doc/user/application_security/policies/scan-result-policies.md b/doc/user/application_security/policies/scan-result-policies.md
index d892012c365..bcc0a27d8b1 100644
--- a/doc/user/application_security/policies/scan-result-policies.md
+++ b/doc/user/application_security/policies/scan-result-policies.md
@@ -257,25 +257,26 @@ You can use this example in the YAML mode of the [Scan Result Policy editor](#sc
It corresponds to a single object from the previous example:
```yaml
-- name: critical vulnerability CS approvals
- description: critical severity level only for container scanning
- enabled: true
- rules:
- - type: scan_finding
- branches:
- - main
- scanners:
- - container_scanning
- vulnerabilities_allowed: 1
- severity_levels:
- - critical
- vulnerability_states:
- - newly_detected
- actions:
- - type: require_approval
- approvals_required: 1
- user_approvers:
- - adalberto.dare
+type: scan_result_policy
+name: critical vulnerability CS approvals
+description: critical severity level only for container scanning
+enabled: true
+rules:
+- type: scan_finding
+ branches:
+ - main
+ scanners:
+ - container_scanning
+ vulnerabilities_allowed: 1
+ severity_levels:
+ - critical
+ vulnerability_states:
+ - newly_detected
+actions:
+- type: require_approval
+ approvals_required: 1
+ user_approvers:
+ - adalberto.dare
```
## Example situations where scan result policies require additional approval
diff --git a/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb b/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
index 2b3841b8f09..8f1ddfbd578 100644
--- a/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
+++ b/gems/gitlab-utils/lib/gitlab/utils/strong_memoize.rb
@@ -44,7 +44,7 @@ module Gitlab
if instance_variable_defined?(expiration_key)
expire_at = instance_variable_get(expiration_key)
- clear_memoization(name) if Time.current > expire_at
+ clear_memoization(name) if expire_at.past?
end
if instance_variable_defined?(key)
diff --git a/lib/gitlab/auth/two_factor_auth_verifier.rb b/lib/gitlab/auth/two_factor_auth_verifier.rb
index fbdfd105ee3..4b66aaf0e6a 100644
--- a/lib/gitlab/auth/two_factor_auth_verifier.rb
+++ b/lib/gitlab/auth/two_factor_auth_verifier.rb
@@ -36,7 +36,7 @@ module Gitlab
return false unless time
- two_factor_grace_period.hours.since(time) < Time.current
+ two_factor_grace_period.hours.since(time).past?
end
def allow_2fa_bypass_for_provider
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 5a0ae680ab8..33e74c90115 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -182,12 +182,12 @@ module Gitlab
request_count_counter.increment
- raise_or_wait_for_rate_limit unless requests_remaining?
+ raise_or_wait_for_rate_limit('Internal threshold reached') unless requests_remaining?
begin
with_retry { yield }
- rescue ::Octokit::TooManyRequests
- raise_or_wait_for_rate_limit
+ rescue ::Octokit::TooManyRequests => e
+ raise_or_wait_for_rate_limit(e.response_body)
# This retry will only happen when running in sequential mode as we'll
# raise an error in parallel mode.
@@ -213,11 +213,11 @@ module Gitlab
octokit.rate_limit.limit
end
- def raise_or_wait_for_rate_limit
+ def raise_or_wait_for_rate_limit(message)
rate_limit_counter.increment
if parallel?
- raise RateLimitError
+ raise RateLimitError, message
else
sleep(rate_limit_resets_in)
end
diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb
index 8ca88859b22..6fe7a0030f0 100644
--- a/lib/gitlab/group_search_results.rb
+++ b/lib/gitlab/group_search_results.rb
@@ -13,7 +13,7 @@ module Gitlab
# rubocop:disable CodeReuse/ActiveRecord
def users
groups = group.self_and_hierarchy_intersecting_with_user_groups(current_user)
- groups = groups.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/417455")
+ groups = groups.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/427108")
members = GroupMember.where(group: groups).non_invite
users = super
diff --git a/lib/gitlab/import_export/project/sample/date_calculator.rb b/lib/gitlab/import_export/project/sample/date_calculator.rb
index 543fd25d883..0cb0eb32a23 100644
--- a/lib/gitlab/import_export/project/sample/date_calculator.rb
+++ b/lib/gitlab/import_export/project/sample/date_calculator.rb
@@ -25,7 +25,7 @@ module Gitlab
end
def calculate_by_closest_date_to_average(date)
- return date unless closest_date_to_average && closest_date_to_average < Time.current
+ return date unless closest_date_to_average && closest_date_to_average.past?
date + (Time.current - closest_date_to_average).seconds
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f4e362f05d1..f595aa585ea 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -25736,6 +25736,9 @@ msgstr ""
msgid "InviteMembersModal|Add unlimited members with your trial"
msgstr ""
+msgid "InviteMembersModal|Administrators can %{linkStart}add new users by email manually%{linkEnd}. After they've been added, you can invite them to this group with their username."
+msgstr ""
+
msgid "InviteMembersModal|Cancel"
msgstr ""
@@ -25769,6 +25772,9 @@ msgstr ""
msgid "InviteMembersModal|Inviting a group %{linkStart}adds its members to your project%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit."
msgstr ""
+msgid "InviteMembersModal|Inviting users by email is disabled"
+msgstr ""
+
msgid "InviteMembersModal|Manage members"
msgstr ""
@@ -25790,6 +25796,9 @@ msgstr ""
msgid "InviteMembersModal|Select a role"
msgstr ""
+msgid "InviteMembersModal|Select members"
+msgstr ""
+
msgid "InviteMembersModal|Select members or type email addresses"
msgstr ""
@@ -25816,6 +25825,9 @@ msgstr ""
msgid "InviteMembersModal|To invite new users to this top-level group, you must remove existing users. You can still add existing users from the top-level group, including any subgroups and projects."
msgstr ""
+msgid "InviteMembersModal|Username"
+msgstr ""
+
msgid "InviteMembersModal|Username or email address"
msgstr ""
@@ -56913,6 +56925,9 @@ msgstr ""
msgid "mrWidget|Assign yourself to this issue"
msgstr ""
+msgid "mrWidget|Auto-merge enabled"
+msgstr ""
+
msgid "mrWidget|Cancel auto-merge"
msgstr ""
diff --git a/package.json b/package.json
index c915abb792a..33826889ae4 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"block-dependencies": "node scripts/frontend/block_dependencies.js",
"check:startup_css": "scripts/frontend/startup_css/startup_css_changed.sh",
"clean": "rm -rf public/assets tmp/cache/*-loader",
- "dev-server": "NODE_OPTIONS=\"--max-old-space-size=5120\" node scripts/frontend/webpack_dev_server.js",
+ "dev-server": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" node scripts/frontend/webpack_dev_server.js",
"file-coverage": "scripts/frontend/file_test_coverage.js",
"lint-docs": "scripts/lint-doc.sh",
"internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue,.graphql",
@@ -44,9 +44,9 @@
"storybook:build": "yarn --cwd ./storybook build --quiet",
"storybook:start": "./scripts/frontend/start_storybook.sh",
"swagger:validate": "swagger-cli validate",
- "webpack": "NODE_OPTIONS=\"--max-old-space-size=5120\" webpack --config config/webpack.config.js",
- "webpack-vendor": "NODE_OPTIONS=\"--max-old-space-size=5120\" webpack --config config/webpack.vendor.config.js",
- "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=5120\" NODE_ENV=production webpack --config config/webpack.config.js"
+ "webpack": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" webpack --config config/webpack.config.js",
+ "webpack-vendor": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" webpack --config config/webpack.vendor.config.js",
+ "webpack-prod": "NODE_OPTIONS=\"${NODE_OPTIONS:=--max-old-space-size=5120}\" NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
"@apollo/client": "^3.5.10",
diff --git a/spec/features/projects/issues/email_participants_spec.rb b/spec/features/projects/issues/email_participants_spec.rb
index a902c8294d7..215c45351c1 100644
--- a/spec/features/projects/issues/email_participants_spec.rb
+++ b/spec/features/projects/issues/email_participants_spec.rb
@@ -35,10 +35,13 @@ RSpec.describe 'viewing an issue', :js, feature_category: :service_desk do
end
context 'when issue is confidential' do
+ let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
+ let!(:confidential_note) { create(:note_on_issue, project: project, noteable: confidential_issue) }
+ let!(:confidential_participants) { create_list(:issue_email_participant, 4, issue: confidential_issue) }
+
before do
- issue.update!(confidential: true)
sign_in(user)
- visit project_issue_path(project, issue)
+ visit project_issue_path(project, confidential_issue)
end
it_behaves_like 'email participants warning in all editors'
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index cfc2fd65cc1..19b7fad5fc8 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
+import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
@@ -60,6 +60,7 @@ describe('InviteMembersModal', () => {
let mock;
let trackingSpy;
const showToast = jest.fn();
+ const newUsersUrl = '/new/users/url';
const expectTracking = (action, label = undefined, property = undefined) =>
expect(trackingSpy).toHaveBeenCalledWith(INVITE_MEMBER_MODAL_TRACKING_CATEGORY, action, {
@@ -68,11 +69,13 @@ describe('InviteMembersModal', () => {
property,
});
- const createComponent = (props = {}, stubs = {}) => {
+ const createComponent = (props = {}, stubs = {}, provide = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
provide: {
newProjectPath,
name: propsData.name,
+ newUsersUrl,
+ ...provide,
},
propsData: {
usersLimitDataset: {},
@@ -129,6 +132,7 @@ describe('InviteMembersModal', () => {
const findEmptyInvitesAlert = () => wrapper.findByTestId('empty-invites-alert');
const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
const findMoreInviteErrorsButton = () => wrapper.findByTestId('accordion-button');
+ const findEmailSignupDisabledAlert = () => wrapper.findByTestId('email-signup-disabled-alert');
const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification);
const findAccordion = () => wrapper.findComponent(GlCollapse);
const findErrorsIcon = () => wrapper.findComponent(GlIcon);
@@ -759,6 +763,58 @@ describe('InviteMembersModal', () => {
expect(findMemberErrorAlert().exists()).toBe(false);
});
});
+
+ describe('when email signup is not allowed', () => {
+ beforeEach(() => {
+ createComponent({}, {}, { isEmailSignupEnabled: false });
+ });
+
+ it('shows the correct form description', () => {
+ expect(membersFormGroupDescription()).toBe('Select members');
+ });
+
+ it('shows an alert', () => {
+ expect(findEmailSignupDisabledAlert().text()).toBe(
+ "Administrators can add new users by email manually. After they've been added, you can invite them to this group with their username.",
+ );
+ });
+
+ it('does not render a link', () => {
+ expect(findEmailSignupDisabledAlert().findComponent(GlLink).exists()).toBe(false);
+ });
+
+ describe('when the current user is an admin', () => {
+ beforeEach(() => {
+ createComponent({}, {}, { isCurrentUserAdmin: true, isEmailSignupEnabled: false });
+ });
+
+ it('shows an alert', () => {
+ expect(findEmailSignupDisabledAlert().text()).toBe(
+ "Administrators can add new users by email manually. After they've been added, you can invite them to this group with their username.",
+ );
+ });
+
+ it('renders a link', () => {
+ expect(findEmailSignupDisabledAlert().findComponent(GlLink).attributes('href')).toBe(
+ newUsersUrl,
+ );
+ });
+
+ describe('when no new users url is provided', () => {
+ beforeEach(() => {
+ createComponent(
+ {},
+ {},
+ { isCurrentUserAdmin: true, isEmailSignupEnabled: false, newUsersUrl: '' },
+ );
+ });
+
+ it('does not render a link', () => {
+ expect(findEmailSignupDisabledAlert().findComponent(GlLink).exists()).toBe(false);
+ });
+ });
+ });
+ });
});
describe('when inviting members and non-members in same click', () => {
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index 925534edd7c..a4b8a8b0197 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -157,6 +157,21 @@ describe('MembersTokenSelect', () => {
expect(tokenSelector.props('allowUserDefinedTokens')).toBe(result);
});
+
+ describe('when cannot use email token', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ canUseEmailToken: false });
+ tokenSelector = findTokenSelector();
+
+ tokenSelector.vm.$emit('text-input', 'foo@bar.com');
+
+ return nextTick();
+ });
+
+ it('does not allow user defined tokens', () => {
+ expect(tokenSelector.props('allowUserDefinedTokens')).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
index e6de9b1de22..94eb47887c3 100644
--- a/spec/frontend/super_sidebar/components/nav_item_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -90,6 +90,19 @@ describe('NavItem component', () => {
expect(findPill().text()).toBe(initialPillValue);
});
});
+
+ describe('async updating pill prop', () => {
+ it('re-renders item with when prop pill_count changes', async () => {
+ createWrapper({ item: { title: 'Foo', pill_count: 0 } });
+
+ expect(findPill().text()).toBe('0');
+
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/428246
+ // This is testing specific async behaviour that was before missed
+ await wrapper.setProps({ item: { title: 'Foo', pill_count: 10 } });
+ expect(findPill().text()).toBe('10');
+ });
+ });
});
describe('destroyed', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js
new file mode 100644
index 00000000000..6b77b869a34
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/rebase_spec.js
@@ -0,0 +1,323 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlModal } from '@gitlab/ui';
+import MergeChecksRebase from '~/vue_merge_request_widget/components/checks/rebase.vue';
+import rebaseQuery from '~/vue_merge_request_widget/queries/states/rebase.query.graphql';
+import eventHub from '~/vue_merge_request_widget/event_hub';
+import toast from '~/vue_shared/plugins/global_toast';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
+
+jest.mock('~/vue_shared/plugins/global_toast');
+
+let wrapper;
+const showMock = jest.fn();
+
+const mockPipelineNodes = [
+ {
+ id: '1',
+ project: {
+ id: '2',
+ fullPath: 'user/forked',
+ },
+ },
+];
+
+const mockQueryHandler = ({
+ rebaseInProgress = false,
+ targetBranch = '',
+ pushToSourceBranch = false,
+ nodes = mockPipelineNodes,
+} = {}) =>
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ id: '2',
+ rebaseInProgress,
+ targetBranch,
+ userPermissions: {
+ pushToSourceBranch,
+ },
+ pipelines: {
+ nodes,
+ },
+ },
+ },
+ },
+ });
+
+const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ return createMockApollo([[rebaseQuery, handler]]);
+};
+
+function createWrapper({ propsData = {}, provideData = {}, handler = mockQueryHandler() } = {}) {
+ wrapper = mountExtended(MergeChecksRebase, {
+ apolloProvider: createMockApolloProvider(handler),
+ provide: {
+ ...provideData,
+ },
+ propsData: {
+ mr: {},
+ service: {},
+ check: {
+ failureReason: '',
+ result: 'failed',
+ },
+ ...propsData,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showMock,
+ },
+ }),
+ },
+ });
+}
+
+describe('Merge request merge checks rebase component', () => {
+ const findStandardRebaseButton = () => wrapper.findByTestId('standard-rebase-button');
+ const findRebaseWithoutCiButton = () => wrapper.findByTestId('rebase-without-ci-button');
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ describe('with permissions', () => {
+ const rebaseMock = jest.fn().mockResolvedValue();
+ const pollMock = jest.fn().mockResolvedValue({});
+
+ describe('Rebase buttons', () => {
+ it('renders both buttons', async () => {
+ createWrapper({
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ createWrapper({
+ propsData: {
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+
+ describe('Rebase when pipelines must succeed is enabled', () => {
+ beforeEach(async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+ });
+
+ it('renders only the rebase button', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(false);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+ });
+
+ describe('Rebase when pipelines must succeed and skipped pipelines are considered successful are enabled', () => {
+ beforeEach(async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ onlyAllowMergeIfPipelineSucceeds: true,
+ allowMergeOnSkippedPipeline: true,
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+ });
+
+ it('renders both rebase buttons', () => {
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ expect(findStandardRebaseButton().exists()).toBe(true);
+ });
+
+ it('starts the rebase when clicking', async () => {
+ findStandardRebaseButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
+ });
+
+ it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
+ });
+ });
+
+ describe('security modal', () => {
+ it('displays modal and rebases after confirming', async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ provideData: { canCreatePipelineInTargetProject: true },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+ expect(showMock).toHaveBeenCalled();
+
+ findModal().vm.$emit('primary');
+
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+
+ it('does not display modal', async () => {
+ createWrapper({
+ propsData: {
+ mr: {
+ sourceProjectFullPath: 'user/forked',
+ targetProjectFullPath: 'root/original',
+ },
+ service: {
+ rebase: rebaseMock,
+ poll: pollMock,
+ },
+ },
+ provideData: { canCreatePipelineInTargetProject: false },
+ handler: mockQueryHandler({ pushToSourceBranch: true }),
+ });
+
+ await waitForPromises();
+
+ findStandardRebaseButton().vm.$emit('click');
+
+ expect(showMock).not.toHaveBeenCalled();
+ expect(rebaseMock).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('without permissions', () => {
+ const exampleTargetBranch = 'fake-branch-to-test-with';
+
+ it('does render the "Rebase without pipeline" button', async () => {
+ createWrapper({
+ handler: mockQueryHandler({
+ rebaseInProgress: false,
+ pushToSourceBranch: false,
+ targetBranch: exampleTargetBranch,
+ }),
+ });
+
+ await waitForPromises();
+
+ expect(findRebaseWithoutCiButton().exists()).toBe(true);
+ });
+ });
+
+ describe('methods', () => {
+ it('checkRebaseStatus', async () => {
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ createWrapper({
+ propsData: {
+ service: {
+ rebase() {
+ return Promise.resolve();
+ },
+ poll() {
+ return Promise.resolve({
+ data: {
+ rebase_in_progress: false,
+ should_be_rebased: false,
+ merge_error: null,
+ },
+ });
+ },
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ findRebaseWithoutCiButton().vm.$emit('click');
+
+ // Wait for the rebase request
+ await nextTick();
+ // Wait for the polling request
+ await nextTick();
+ // Wait for the eventHub to be called
+ await nextTick();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetRebaseSuccess');
+ expect(toast).toHaveBeenCalledWith('Rebase completed');
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
index c86fe6d0a10..6224d6e42ee 100644
--- a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
@@ -31,6 +31,7 @@ function factory({ canMerge = true, mergeChecks = [] } = {}) {
apolloProvider,
propsData: {
mr: {},
+ service: {},
},
});
}
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 48b86d879ad..9239807ae71 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -831,4 +831,16 @@ describe('ReadyToMerge', () => {
});
});
});
+
+ describe('merge details', () => {
+ it('shows auto-merge hint when auto merge is set and some checks have failed', () => {
+ createComponent({ mr: { state: 'mergeChecksFailed', autoMergeEnabled: true } });
+ expect(wrapper.text()).toContain('Auto-merge enabled');
+ });
+
+ it("doesn't show auto-merge hint when auto merge is not set", () => {
+ createComponent({ mr: { autoMergeEnabled: false } });
+ expect(wrapper.text()).not.toContain('Auto-merge enabled');
+ });
+ });
});
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 5f321a15de9..c409ec6983f 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -278,7 +278,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
client.with_rate_limit do
if retries == 0
retries += 1
- raise(Octokit::TooManyRequests)
+ raise(Octokit::TooManyRequests.new(body: 'primary'))
end
end
@@ -306,6 +306,37 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
expect(client.with_rate_limit { 10 }).to eq(10)
end
+ context 'when threshold is hit' do
+ it 'raises a RateLimitError with the appropriate message' do
+ expect(client).to receive(:requests_remaining?).and_return(false)
+
+ expect { client.with_rate_limit }
+ .to raise_error(Gitlab::GithubImport::RateLimitError, 'Internal threshold reached')
+ end
+ end
+
+ context 'when primary rate limit hit' do
+ let(:limited_block) { -> { raise(Octokit::TooManyRequests.new(body: 'primary')) } }
+
+ it 're-raises a RateLimitError with the appropriate message' do
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ expect { client.with_rate_limit(&limited_block) }
+ .to raise_error(Gitlab::GithubImport::RateLimitError, 'primary')
+ end
+ end
+
+ context 'when secondary rate limit hit' do
+ let(:limited_block) { -> { raise(Octokit::TooManyRequests.new(body: 'secondary')) } }
+
+ it 're-raises a RateLimitError with the appropriate message' do
+ expect(client).to receive(:requests_remaining?).and_return(true)
+
+ expect { client.with_rate_limit(&limited_block) }
+ .to raise_error(Gitlab::GithubImport::RateLimitError, 'secondary')
+ end
+ end
+
context 'when Faraday error received from octokit', :aggregate_failures do
let(:error_class) { described_class::CLIENT_CONNECTION_ERROR }
let(:info_params) { { 'error.class': error_class } }
@@ -392,7 +423,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
describe '#raise_or_wait_for_rate_limit' do
context 'when running in parallel mode' do
it 'raises RateLimitError' do
- expect { client.raise_or_wait_for_rate_limit }
+ expect { client.raise_or_wait_for_rate_limit('primary') }
.to raise_error(Gitlab::GithubImport::RateLimitError)
end
end
@@ -404,7 +435,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
expect(client).to receive(:rate_limit_resets_in).and_return(1)
expect(client).to receive(:sleep).with(1)
- client.raise_or_wait_for_rate_limit
+ client.raise_or_wait_for_rate_limit('primary')
end
it 'increments the rate limit counter' do
@@ -420,7 +451,7 @@ RSpec.describe Gitlab::GithubImport::Client, feature_category: :importers do
.to receive(:increment)
.and_call_original
- client.raise_or_wait_for_rate_limit
+ client.raise_or_wait_for_rate_limit('primary')
end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 2a5d781edc7..e2e13ea0e17 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2230,7 +2230,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'when artifacts do not expire' do
- it { is_expected.to eq(false) }
+ it { is_expected.to be_falsey }
end
context 'when artifacts expire in the future' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 96ef36a5b75..0ae4b35af7a 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -2053,29 +2053,6 @@ RSpec.describe Group, feature_category: :groups_and_projects do
end
end
- describe '#project_users_with_descendants' do
- let(:user_a) { create(:user) }
- let(:user_b) { create(:user) }
- let(:user_c) { create(:user) }
-
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, parent: group) }
- let(:deep_nested_group) { create(:group, parent: nested_group) }
- let(:project_a) { create(:project, namespace: group) }
- let(:project_b) { create(:project, namespace: nested_group) }
- let(:project_c) { create(:project, namespace: deep_nested_group) }
-
- it 'returns members of all projects in group and subgroups' do
- project_a.add_developer(user_a)
- project_b.add_developer(user_b)
- project_c.add_developer(user_c)
-
- expect(group.project_users_with_descendants).to contain_exactly(user_a, user_b, user_c)
- expect(nested_group.project_users_with_descendants).to contain_exactly(user_b, user_c)
- expect(deep_nested_group.project_users_with_descendants).to contain_exactly(user_c)
- end
- end
-
describe '#refresh_members_authorized_projects' do
let_it_be(:group) { create(:group, :nested) }
let_it_be(:parent_group_user) { create(:user) }
diff --git a/spec/services/packages/npm/create_package_service_spec.rb b/spec/services/packages/npm/create_package_service_spec.rb
index 1c935c27d7f..7336867f5ea 100644
--- a/spec/services/packages/npm/create_package_service_spec.rb
+++ b/spec/services/packages/npm/create_package_service_spec.rb
@@ -332,6 +332,84 @@ RSpec.describe Packages::Npm::CreatePackageService, feature_category: :package_r
end
end
+ context 'when feature flag :packages_package_protection is disabled' do
+ before do
+ stub_feature_flags(packages_protected_packages: false)
+ end
+
+ context 'with matching package protection rule for all roles' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:package_name_pattern_no_match) { "#{package_name}_no_match" }
+
+ where(:package_name_pattern, :push_protected_up_to_access_level) do
+ ref(:package_name) | :developer
+ ref(:package_name) | :owner
+ ref(:package_name_pattern_no_match) | :developer
+ ref(:package_name_pattern_no_match) | :owner
+ end
+
+ with_them do
+ let!(:package_protection_rule) do
+ create(
+ :package_protection_rule,
+ package_name_pattern: package_name_pattern,
+ package_type: :npm,
+ project: project,
+ push_protected_up_to_access_level: push_protected_up_to_access_level
+ )
+ end
+
+ it_behaves_like 'valid package'
+ end
+ end
+ end
+
+ context 'with package protection rule for different roles and package_name_patterns' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project_developer) { create(:user).tap { |u| project.add_developer(u) } }
+ let(:project_maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
+ let(:project_owner) { project.owner }
+
+ let(:package_name_pattern_no_match) { "#{package_name}_no_match" }
+
+ shared_examples 'protected package' do
+ it { is_expected.to include http_status: 403, message: 'Package protected.' }
+
+ it 'does not create any npm-related package records' do
+ expect { subject }
+ .to not_change { Packages::Package.count }
+ .and not_change { Packages::Package.npm.count }
+ .and not_change { Packages::Tag.count }
+ .and not_change { Packages::Npm::Metadatum.count }
+ end
+ end
+
+ where(:package_name_pattern, :push_protected_up_to_access_level, :user, :shared_examples_name) do
+ ref(:package_name) | :developer | ref(:project_developer) | 'protected package'
+ ref(:package_name) | :developer | ref(:project_owner) | 'valid package'
+ ref(:package_name) | :maintainer | ref(:project_maintainer) | 'protected package'
+ ref(:package_name) | :owner | ref(:project_owner) | 'protected package'
+ ref(:package_name_pattern_no_match) | :developer | ref(:project_owner) | 'valid package'
+ ref(:package_name_pattern_no_match) | :owner | ref(:project_owner) | 'valid package'
+ end
+
+ with_them do
+ let!(:package_protection_rule) do
+ create(
+ :package_protection_rule,
+ package_name_pattern: package_name_pattern,
+ package_type: :npm,
+ project: project,
+ push_protected_up_to_access_level: push_protected_up_to_access_level
+ )
+ end
+
+ it_behaves_like params[:shared_examples_name]
+ end
+ end
+
def create_packages(project, user, params)
with_threads do
described_class.new(project, user, params).execute
diff --git a/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
index 5beb11c64aa..8c67583f6b5 100644
--- a/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/finish_batched_pipeline_worker_spec.rb
@@ -13,32 +13,13 @@ RSpec.describe BulkImports::FinishBatchedPipelineWorker, feature_category: :impo
subject(:worker) { described_class.new }
describe '#perform' do
- context 'when job version is nil' do
- before do
- allow(subject).to receive(:job_version).and_return(nil)
- end
-
- it 'finishes pipeline and enqueues entity worker' do
- expect(BulkImports::EntityWorker).to receive(:perform_async)
- .with(entity.id)
-
- subject.perform(pipeline_tracker.id)
-
- expect(pipeline_tracker.reload.finished?).to eq(true)
- end
- end
-
- context 'when job version is present' do
- it 'finishes pipeline and does not enqueues entity worker' do
- expect(BulkImports::EntityWorker).not_to receive(:perform_async)
-
- subject.perform(pipeline_tracker.id)
-
- expect(pipeline_tracker.reload.finished?).to eq(true)
+ context 'when import is in progress' do
+ it 'marks the pipeline as finished' do
+ expect { subject.perform(pipeline_tracker.id) }
+ .to change { pipeline_tracker.reload.finished? }
+ .from(false).to(true)
end
- end
- context 'when import is in progress' do
it 're-enqueues for any started batches' do
create(:bulk_import_batch_tracker, :started, tracker: pipeline_tracker)
diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb
index e1259d5666d..2042300edf0 100644
--- a/spec/workers/bulk_imports/pipeline_worker_spec.rb
+++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb
@@ -63,36 +63,6 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do
expect(pipeline_tracker.jid).to eq('jid')
end
- context 'when job version is nil' do
- before do
- allow(subject).to receive(:job_version).and_return(nil)
- end
-
- it 'runs the given pipeline successfully and enqueues entity worker' do
- expect(BulkImports::EntityWorker).to receive(:perform_async).with(entity.id)
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
-
- pipeline_tracker.reload
-
- expect(pipeline_tracker.status_name).to eq(:finished)
- end
-
- context 'when an error occurs' do
- it 'enqueues entity worker' do
- expect_next_instance_of(pipeline_class) do |pipeline|
- expect(pipeline)
- .to receive(:run)
- .and_raise(StandardError, 'Error!')
- end
-
- expect(BulkImports::EntityWorker).to receive(:perform_async).with(entity.id)
-
- subject.perform(pipeline_tracker.id, pipeline_tracker.stage, entity.id)
- end
- end
- end
-
context 'when exclusive lease cannot be obtained' do
it 'does not run the pipeline' do
expect(subject).to receive(:try_obtain_lease).and_return(false)
diff --git a/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock b/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock
index 617ee7d91f5..47191da2a01 100644
--- a/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock
+++ b/vendor/gems/devise-pbkdf2-encryptable/Gemfile.lock
@@ -3,85 +3,115 @@ PATH
specs:
devise-pbkdf2-encryptable (0.0.0)
devise (~> 4.0)
- devise-two-factor (~> 4.0)
+ devise-two-factor (~> 4.1.1)
GEM
remote: https://rubygems.org/
specs:
- actionpack (6.1.6)
- actionview (= 6.1.6)
- activesupport (= 6.1.6)
- rack (~> 2.0, >= 2.0.9)
+ actionpack (7.1.1)
+ actionview (= 7.1.1)
+ activesupport (= 7.1.1)
+ nokogiri (>= 1.8.5)
+ rack (>= 2.2.4)
+ rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actionview (6.1.6)
- activesupport (= 6.1.6)
+ rails-dom-testing (~> 2.2)
+ rails-html-sanitizer (~> 1.6)
+ actionview (7.1.1)
+ activesupport (= 7.1.1)
builder (~> 3.1)
- erubi (~> 1.4)
- rails-dom-testing (~> 2.0)
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
- activemodel (6.1.6)
- activesupport (= 6.1.6)
- activesupport (6.1.6)
+ erubi (~> 1.11)
+ rails-dom-testing (~> 2.2)
+ rails-html-sanitizer (~> 1.6)
+ activemodel (7.1.1)
+ activesupport (= 7.1.1)
+ activesupport (7.1.1)
+ base64
+ bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
+ connection_pool (>= 2.2.5)
+ drb
i18n (>= 1.6, < 2)
minitest (>= 5.1)
+ mutex_m
tzinfo (~> 2.0)
- zeitwerk (~> 2.3)
- attr_encrypted (3.1.0)
+ attr_encrypted (4.0.0)
encryptor (~> 3.0.0)
- bcrypt (3.1.18)
+ base64 (0.1.1)
+ bcrypt (3.1.19)
+ bigdecimal (3.1.4)
builder (3.2.4)
- concurrent-ruby (1.1.10)
+ concurrent-ruby (1.2.2)
+ connection_pool (2.4.1)
crass (1.0.6)
- devise (4.8.1)
+ devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
responders
warden (~> 1.2.3)
- devise-two-factor (4.0.2)
- activesupport (< 7.1)
- attr_encrypted (>= 1.3, < 4, != 2)
+ devise-two-factor (4.1.1)
+ activesupport (~> 7.0)
+ attr_encrypted (>= 1.3, < 5, != 2)
devise (~> 4.0)
- railties (< 7.1)
+ railties (~> 7.0)
rotp (~> 6.0)
diff-lcs (1.5.0)
+ drb (2.1.1)
+ ruby2_keywords
encryptor (3.0.0)
- erubi (1.10.0)
- i18n (1.10.0)
+ erubi (1.12.0)
+ i18n (1.14.1)
concurrent-ruby (~> 1.0)
- loofah (2.18.0)
+ io-console (0.6.0)
+ irb (1.8.3)
+ rdoc
+ reline (>= 0.3.8)
+ loofah (2.21.4)
crass (~> 1.0.2)
- nokogiri (>= 1.5.9)
- method_source (1.0.0)
- mini_portile2 (2.8.0)
- minitest (5.16.0)
- nokogiri (1.13.6)
- mini_portile2 (~> 2.8.0)
+ nokogiri (>= 1.12.0)
+ mini_portile2 (2.8.4)
+ minitest (5.20.0)
+ mutex_m (0.1.2)
+ nokogiri (1.15.4)
+ mini_portile2 (~> 2.8.2)
racc (~> 1.4)
orm_adapter (0.5.0)
- racc (1.6.0)
- rack (2.2.3.1)
- rack-test (1.1.0)
- rack (>= 1.0, < 3)
- rails-dom-testing (2.0.3)
- activesupport (>= 4.2.0)
+ psych (5.1.1.1)
+ stringio
+ racc (1.7.1)
+ rack (3.0.8)
+ rack-session (2.0.0)
+ rack (>= 3.0.0)
+ rack-test (2.1.0)
+ rack (>= 1.3)
+ rackup (2.1.0)
+ rack (>= 3)
+ webrick (~> 1.8)
+ rails-dom-testing (2.2.0)
+ activesupport (>= 5.0.0)
+ minitest
nokogiri (>= 1.6)
- rails-html-sanitizer (1.4.3)
- loofah (~> 2.3)
- railties (6.1.6)
- actionpack (= 6.1.6)
- activesupport (= 6.1.6)
- method_source
+ rails-html-sanitizer (1.6.0)
+ loofah (~> 2.21)
+ nokogiri (~> 1.14)
+ railties (7.1.1)
+ actionpack (= 7.1.1)
+ activesupport (= 7.1.1)
+ irb
+ rackup (>= 1.0.0)
rake (>= 12.2)
- thor (~> 1.0)
+ thor (~> 1.0, >= 1.2.2)
+ zeitwerk (~> 2.6)
rake (13.0.6)
- responders (3.0.1)
- actionpack (>= 5.0)
- railties (>= 5.0)
- rotp (6.2.0)
+ rdoc (6.5.0)
+ psych (>= 4.0.0)
+ reline (0.3.9)
+ io-console (~> 0.5)
+ responders (3.1.1)
+ actionpack (>= 5.2)
+ railties (>= 5.2)
+ rotp (6.3.0)
rspec (3.10.0)
rspec-core (~> 3.10.0)
rspec-expectations (~> 3.10.0)
@@ -95,18 +125,21 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.10.0)
rspec-support (3.10.3)
- thor (1.2.1)
- tzinfo (2.0.4)
+ ruby2_keywords (0.0.5)
+ stringio (3.0.8)
+ thor (1.2.2)
+ tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
warden (1.2.9)
rack (>= 2.0.9)
- zeitwerk (2.6.0)
+ webrick (1.8.1)
+ zeitwerk (2.6.12)
PLATFORMS
ruby
DEPENDENCIES
- activemodel (~> 6.1, < 8)
+ activemodel (~> 7.0, < 8)
devise-pbkdf2-encryptable!
rspec (~> 3.10.0)
diff --git a/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec b/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec
index 9c7e3dd5af5..cd2c62b457d 100644
--- a/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec
+++ b/vendor/gems/devise-pbkdf2-encryptable/devise-pbkdf2-encryptable.gemspec
@@ -19,8 +19,8 @@ Gem::Specification.new do |spec|
spec.version = '0.0.0'
spec.add_runtime_dependency 'devise', '~> 4.0'
- spec.add_runtime_dependency 'devise-two-factor', '~> 4.0'
+ spec.add_runtime_dependency 'devise-two-factor', '~> 4.1.1'
- spec.add_development_dependency 'activemodel', '~> 6.1', '< 8'
+ spec.add_development_dependency 'activemodel', '~> 7.0', '< 8'
spec.add_development_dependency 'rspec', '~> 3.10.0'
end