diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-28 18:08:32 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-28 18:08:32 +0000 |
commit | 36eff6e5089629619cc55f4771fa949d6ae2b29b (patch) | |
tree | 6381b0c90f403c535abdde2f712cd346a78770fe /app | |
parent | baed745d21710f1d78ece03558873acd6fd7d358 (diff) | |
download | gitlab-ce-36eff6e5089629619cc55f4771fa949d6ae2b29b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
34 files changed, 808 insertions, 62 deletions
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js index 4b337dce8f3..834defe336b 100644 --- a/app/assets/javascripts/behaviors/copy_to_clipboard.js +++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js @@ -10,10 +10,10 @@ const CLIPBOARD_ERROR_EVENT = 'clipboard-error'; const I18N_ERROR_MESSAGE = __('Copy failed. Please manually copy the value.'); function showTooltip(target, title) { - const { title: originalTitle } = target.dataset; + const { originalTitle } = target.dataset; once('hidden', (tooltip) => { - if (tooltip.target === target) { + if (originalTitle && tooltip.target === target) { target.setAttribute('title', originalTitle); target.setAttribute('aria-label', originalTitle); fixTitle(target); diff --git a/app/assets/javascripts/ci/runner/admin_register_runner/admin_register_runner_app.vue b/app/assets/javascripts/ci/runner/admin_register_runner/admin_register_runner_app.vue new file mode 100644 index 00000000000..b291be41203 --- /dev/null +++ b/app/assets/javascripts/ci/runner/admin_register_runner/admin_register_runner_app.vue @@ -0,0 +1,81 @@ +<script> +import { GlButton } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import { createAlert } from '~/flash'; +import { getParameterByName } from '~/lib/utils/url_utility'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants'; +import runnerForRegistrationQuery from '../graphql/register/runner_for_registration.query.graphql'; +import { I18N_FETCH_ERROR, PARAM_KEY_PLATFORM, DEFAULT_PLATFORM } from '../constants'; +import RegistrationInstructions from '../components/registration/registration_instructions.vue'; +import { captureException } from '../sentry_utils'; + +export default { + name: 'AdminRegisterRunnerApp', + components: { + GlButton, + RegistrationInstructions, + }, + props: { + runnerId: { + type: String, + required: true, + }, + runnersPath: { + type: String, + required: true, + }, + }, + data() { + return { + platform: getParameterByName(PARAM_KEY_PLATFORM) || DEFAULT_PLATFORM, + runner: null, + }; + }, + apollo: { + runner: { + query: runnerForRegistrationQuery, + variables() { + return { + id: convertToGraphQLId(TYPENAME_CI_RUNNER, this.runnerId), + }; + }, + error(error) { + createAlert({ message: I18N_FETCH_ERROR }); + captureException({ error, component: this.$options.name }); + }, + }, + }, + computed: { + description() { + return this.runner?.description; + }, + heading() { + if (this.description) { + return sprintf(s__('Runners|Register "%{runnerDescription}" runner'), { + runnerDescription: this.description, + }); + } + return s__('Runners|Register runner'); + }, + ephemeralAuthenticationToken() { + return this.runner?.ephemeralAuthenticationToken; + }, + }, +}; +</script> +<template> + <div> + <h1 class="gl-font-size-h1">{{ heading }}</h1> + + <registration-instructions + :loading="$apollo.queries.runner.loading" + :platform="platform" + :token="ephemeralAuthenticationToken" + /> + + <gl-button :href="runnersPath" variant="confirm">{{ + s__('Runners|Go to runners page') + }}</gl-button> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/admin_register_runner/index.js b/app/assets/javascripts/ci/runner/admin_register_runner/index.js index edb2ec65e98..bd43a5e8ce9 100644 --- a/app/assets/javascripts/ci/runner/admin_register_runner/index.js +++ b/app/assets/javascripts/ci/runner/admin_register_runner/index.js @@ -1,5 +1,36 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage'; +import AdminRegisterRunnerApp from './admin_register_runner_app.vue'; -export const initAdminRegisterRunner = () => { +Vue.use(VueApollo); + +export const initAdminRegisterRunner = (selector = '#js-admin-register-runner') => { showAlertFromLocalStorage(); + + const el = document.querySelector(selector); + + if (!el) { + return null; + } + + const { runnerId, runnersPath } = el.dataset; + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(AdminRegisterRunnerApp, { + props: { + runnerId, + runnersPath, + }, + }); + }, + }); }; diff --git a/app/assets/javascripts/ci/runner/components/registration/cli_command.vue b/app/assets/javascripts/ci/runner/components/registration/cli_command.vue new file mode 100644 index 00000000000..95b135c83a7 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/cli_command.vue @@ -0,0 +1,42 @@ +<script> +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +export default { + components: { + ClipboardButton, + }, + props: { + prompt: { + type: String, + required: false, + default: '', + }, + command: { + type: [Array, String], + required: false, + default: '', + }, + }, + computed: { + lines() { + if (typeof this.command === 'string') { + return [this.command]; + } + return this.command; + }, + clipboard() { + return this.lines.join(''); + }, + }, +}; +</script> +<template> + <div class="gl-display-flex gl-gap-3 gl-align-items-flex-start"> + <!-- eslint-disable vue/require-v-for-key--> + <pre + class="gl-w-full" + ><span v-if="prompt" class="gl-user-select-none">{{ prompt }} </span><template v-for="line in lines">{{ line }}<br class="gl-user-select-none"/></template></pre> + <!-- eslint-enable vue/require-v-for-key--> + <clipboard-button :text="clipboard" :title="__('Copy')" /> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue new file mode 100644 index 00000000000..e01d8b64839 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue @@ -0,0 +1,142 @@ +<script> +import { GlIcon, GlLink, GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +import { INSTALL_HELP_URL, EXECUTORS_HELP_URL, SERVICE_COMMANDS_HELP_URL } from '../../constants'; +import CliCommand from './cli_command.vue'; +import { commandPrompt, registerCommand, runCommand } from './utils'; + +export default { + components: { + GlIcon, + GlLink, + GlSkeletonLoader, + GlSprintf, + ClipboardButton, + CliCommand, + }, + props: { + platform: { + type: String, + required: true, + }, + loading: { + type: Boolean, + required: false, + default: false, + }, + token: { + type: String, + required: false, + default: null, + }, + }, + computed: { + commandPrompt() { + return commandPrompt({ platform: this.platform }); + }, + registerCommand() { + return registerCommand({ platform: this.platform, registrationToken: this.token }); + }, + runCommand() { + return runCommand({ platform: this.platform }); + }, + }, + INSTALL_HELP_URL, + EXECUTORS_HELP_URL, + SERVICE_COMMANDS_HELP_URL, +}; +</script> +<template> + <div> + <p> + <gl-sprintf + :message=" + s__( + 'Runners|GitLab Runner must be installed before you can register a runner. %{linkStart}How do I install GitLab Runner?%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link :href="$options.INSTALL_HELP_URL">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + + <section> + <h2 class="gl-font-size-h2">{{ s__('Runners|Step 1') }}</h2> + <p> + {{ + s__( + 'Runners|Copy and paste the following command into your command line to register the runner.', + ) + }} + </p> + <gl-skeleton-loader v-if="loading" /> + <template v-else> + <cli-command :prompt="commandPrompt" :command="registerCommand" /> + <p> + <gl-icon name="information-o" class="gl-text-blue-600!" /> + <gl-sprintf + :message=" + s__( + 'Runners|The %{boldStart}runner token%{boldEnd} %{token} displays %{boldStart}only for a short time%{boldEnd}, and is stored in the %{codeStart}config.toml%{codeEnd} after you create the runner. It will not be visible once the runner is registered.', + ) + " + > + <template #token> + <code>{{ token }}</code> + <clipboard-button + :text="token" + :title="__('Copy')" + size="small" + category="tertiary" + class="gl-border-none!" + /> + </template> + <template #bold="{ content }" + ><span class="gl-font-weight-bold">{{ content }}</span></template + > + <template #code="{ content }" + ><code>{{ content }}</code></template + > + </gl-sprintf> + </p> + </template> + </section> + <section> + <h2 class="gl-font-size-h2">{{ s__('Runners|Step 2') }}</h2> + <p> + <gl-sprintf + :message=" + s__( + 'Runners|Choose an executor when prompted by the command line. Executors run builds in different environments. %{linkStart}Not sure which one to select?%{linkEnd}', + ) + " + > + <template #link="{ content }"> + <gl-link :href="$options.EXECUTORS_HELP_URL">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + </section> + <section> + <h2 class="gl-font-size-h2">{{ s__('Runners|Optional. Step 3') }}</h2> + <p>{{ s__('Runners|Manually verify that the runner is available to pick up jobs.') }}</p> + <cli-command :prompt="commandPrompt" :command="runCommand" /> + <p> + <gl-sprintf + :message=" + s__( + 'Runners|This may not be needed if you manage your runner as a %{linkStart}system or user service%{linkEnd}.', + ) + " + > + <template #link="{ content }"> + <gl-link :href="$options.SERVICE_COMMANDS_HELP_URL">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + </section> + </div> +</template> diff --git a/app/assets/javascripts/ci/runner/components/registration/utils.js b/app/assets/javascripts/ci/runner/components/registration/utils.js new file mode 100644 index 00000000000..32fb8eac5e9 --- /dev/null +++ b/app/assets/javascripts/ci/runner/components/registration/utils.js @@ -0,0 +1,43 @@ +import { + DEFAULT_PLATFORM, + LINUX_PLATFORM, + MACOS_PLATFORM, + WINDOWS_PLATFORM, +} from '../../constants'; + +/* eslint-disable @gitlab/require-i18n-strings */ +const OS = { + [LINUX_PLATFORM]: { + commandPrompt: '$', + executable: 'gitlab-runner', + }, + [MACOS_PLATFORM]: { + commandPrompt: '$', + executable: 'gitlab-runner', + }, + [WINDOWS_PLATFORM]: { + commandPrompt: '>', + executable: '.\\gitlab-runner.exe', + }, +}; + +export const commandPrompt = ({ platform }) => { + return (OS[platform] || OS[DEFAULT_PLATFORM]).commandPrompt; +}; + +export const executable = ({ platform }) => { + return (OS[platform] || OS[DEFAULT_PLATFORM]).executable; +}; + +export const registerCommand = ({ platform, url = gon.gitlab_url, registrationToken }) => { + return [ + `${executable({ platform })} register`, + ` --url ${url}`, + ` --registration-token ${registrationToken}`, + ]; +}; + +export const runCommand = ({ platform }) => { + return `${executable({ platform })} run`; +}; +/* eslint-enable @gitlab/require-i18n-strings */ diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js index 27c02420036..1db4ff68872 100644 --- a/app/assets/javascripts/ci/runner/constants.js +++ b/app/assets/javascripts/ci/runner/constants.js @@ -188,5 +188,9 @@ export const DEFAULT_PLATFORM = LINUX_PLATFORM; // Runner docs are in a separate repository and are not shipped with GitLab // they are rendered as external URLs. +export const INSTALL_HELP_URL = 'https://docs.gitlab.com/runner/install'; +export const EXECUTORS_HELP_URL = 'https://docs.gitlab.com/runner/executors/'; +export const SERVICE_COMMANDS_HELP_URL = + 'https://docs.gitlab.com/runner/commands/#service-related-commands'; export const DOCKER_HELP_URL = 'https://docs.gitlab.com/runner/install/docker.html'; export const KUBERNETES_HELP_URL = 'https://docs.gitlab.com/runner/install/kubernetes.html'; diff --git a/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql b/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql new file mode 100644 index 00000000000..a26d43c3729 --- /dev/null +++ b/app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql @@ -0,0 +1,7 @@ +query getRunnerForRegistration($id: CiRunnerID!) { + runner(id: $id) { + id + description + ephemeralAuthenticationToken + } +} diff --git a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue index 149049247fb..accf4e77043 100644 --- a/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue +++ b/app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue @@ -66,7 +66,7 @@ export default { <template> <div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)"> <div class="card card-slim gl-mt-5 gl-mb-0"> - <div class="card-header gl-bg-gray-10"> + <div class="card-header gl-px-5 gl-py-4 gl-bg-white"> <div class="card-title gl-relative gl-display-flex gl-align-items-center gl-line-height-20 gl-font-weight-bold gl-m-0" > @@ -79,7 +79,7 @@ export default { {{ __('Related merge requests') }} </h3> <template v-if="totalCount"> - <gl-icon name="merge-request" class="gl-ml-5 gl-mr-2 gl-text-gray-500" /> + <gl-icon name="merge-request" class="gl-ml-3 gl-mr-2 gl-text-gray-500" /> <span data-testid="count">{{ totalCount }}</span> </template> </div> @@ -90,7 +90,7 @@ export default { label="Fetching related merge requests" class="gl-py-3" /> - <ul v-else class="content-list related-items-list"> + <ul v-else class="content-list related-items-list gl-bg-gray-10"> <li v-for="mr in mergeRequests" :key="mr.id" class="list-item gl-m-0! gl-p-0!"> <related-issuable-item :id-key="mr.id" diff --git a/app/assets/javascripts/related_issues/components/related_issues_block.vue b/app/assets/javascripts/related_issues/components/related_issues_block.vue index 4a130ade631..4aebaa86932 100644 --- a/app/assets/javascripts/related_issues/components/related_issues_block.vue +++ b/app/assets/javascripts/related_issues/components/related_issues_block.vue @@ -187,7 +187,7 @@ export default { 'gl-border-b-1': isOpen, 'gl-border-b-0': !isOpen, }" - class="gl-display-flex gl-justify-content-space-between gl-line-height-24 gl-py-3 gl-px-5 gl-bg-gray-10 gl-border-b-solid gl-border-b-gray-100" + class="gl-display-flex gl-justify-content-space-between gl-line-height-24 gl-pl-5 gl-pr-4 gl-py-4 gl-bg-white gl-border-b-solid gl-border-b-gray-100" > <h3 class="card-title h5 gl-my-0 gl-display-flex gl-align-items-center gl-flex-grow-1"> <gl-link diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index 5e24f20f40a..fb23a4f2deb 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -126,7 +126,7 @@ export default { <gl-disclosure-dropdown ref="dropdown"> <template #toggle> <gl-button category="tertiary" icon="question-o" class="btn-with-notification"> - <span v-if="showWhatsNewNotification" class="notification"></span> + <span v-if="showWhatsNewNotification" class="notification-dot-info"></span> {{ $options.i18n.help }} </gl-button> </template> diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue index 7700b7fa3f8..103501e86ef 100644 --- a/app/assets/javascripts/super_sidebar/components/user_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue @@ -9,6 +9,8 @@ import { import SafeHtml from '~/vue_shared/directives/safe_html'; import { s__, __, sprintf } from '~/locale'; import NewNavToggle from '~/nav/components/new_nav_toggle.vue'; +import Tracking from '~/tracking'; +import PersistentUserCallout from '~/persistent_user_callout'; import UserNameGroup from './user_name_group.vue'; export default { @@ -18,13 +20,13 @@ export default { badgeLabel: s__('NorthstarNavigation|Alpha'), sectionTitle: s__('NorthstarNavigation|Navigation redesign'), }, - user: { - setStatus: s__('SetStatusModal|Set status'), - editStatus: s__('SetStatusModal|Edit status'), - editProfile: s__('CurrentUser|Edit profile'), - preferences: s__('CurrentUser|Preferences'), - gitlabNext: s__('CurrentUser|Switch to GitLab Next'), - }, + setStatus: s__('SetStatusModal|Set status'), + editStatus: s__('SetStatusModal|Edit status'), + editProfile: s__('CurrentUser|Edit profile'), + preferences: s__('CurrentUser|Preferences'), + buyPipelineMinutes: s__('CurrentUser|Buy Pipeline minutes'), + oneOfGroupsRunningOutOfPipelineMinutes: s__('CurrentUser|One of your groups is running out'), + gitlabNext: s__('CurrentUser|Switch to GitLab Next'), provideFeedback: s__('NorthstarNavigation|Provide feedback'), startTrial: s__('CurrentUser|Start an Ultimate trial'), signOut: __('Sign out'), @@ -41,6 +43,7 @@ export default { directives: { SafeHtml, }, + mixins: [Tracking.mixin()], inject: ['toggleNewNavEndpoint'], props: { data: { @@ -56,7 +59,7 @@ export default { const { busy, customized } = this.data.status; const statusLabel = - busy || customized ? this.$options.i18n.user.editStatus : this.$options.i18n.user.setStatus; + busy || customized ? this.$options.i18n.editStatus : this.$options.i18n.setStatus; return { text: statusLabel, @@ -73,19 +76,32 @@ export default { }, editProfileItem() { return { - text: this.$options.i18n.user.editProfile, + text: this.$options.i18n.editProfile, href: this.data.settings.profile_path, }; }, preferencesItem() { return { - text: this.$options.i18n.user.preferences, + text: this.$options.i18n.preferences, href: this.data.settings.profile_preferences_path, }; }, + addBuyPipelineMinutesMenuItem() { + return this.data.pipeline_minutes?.show_buy_pipeline_minutes; + }, + buyPipelineMinutesItem() { + return { + text: this.$options.i18n.buyPipelineMinutes, + warningText: this.$options.i18n.oneOfGroupsRunningOutOfPipelineMinutes, + href: this.data.pipeline_minutes?.buy_pipeline_minutes_path, + extraAttrs: { + class: 'js-follow-link', + }, + }; + }, gitlabNextItem() { return { - text: this.$options.i18n.user.gitlabNext, + text: this.$options.i18n.gitlabNext, href: this.data.canary_toggle_com_url, }; }, @@ -130,6 +146,38 @@ export default { 'data-current-clear-status-after': this.data.status.clear_after, }; }, + buyPipelineMinutesCalloutData() { + return this.showNotificationDot + ? { + 'data-feature-id': this.data.pipeline_minutes.callout_attrs.feature_id, + 'data-dismiss-endpoint': this.data.pipeline_minutes.callout_attrs.dismiss_endpoint, + } + : {}; + }, + showNotificationDot() { + return this.data.pipeline_minutes?.show_notification_dot; + }, + }, + methods: { + onShow() { + this.trackEvents(); + this.initCallout(); + }, + initCallout() { + if (this.showNotificationDot) { + PersistentUserCallout.factory(this.$refs?.buyPipelineMinutesNotificationCallout.$el); + } + }, + trackEvents() { + if (this.addBuyPipelineMinutesMenuItem) { + const { + 'track-action': trackAction, + 'track-label': label, + 'track-property': property, + } = this.data.pipeline_minutes.tracking_attrs; + this.track(trackAction, { label, property }); + } + }, }, }; </script> @@ -140,9 +188,10 @@ export default { placement="right" data-testid="user-dropdown" data-qa-selector="user_menu" + @shown="onShow" > <template #toggle> - <button class="user-bar-item"> + <button class="user-bar-item btn-with-notification"> <span class="gl-sr-only">{{ toggleText }}</span> <gl-avatar :size="24" @@ -151,6 +200,13 @@ export default { aria-hidden="true" data-qa-selector="user_avatar_content" /> + <span + v-if="showNotificationDot" + class="notification-dot-warning" + data-testid="buy-pipeline-minutes-notification-dot" + v-bind="data.pipeline_minutes.notification_dot_attrs" + > + </span> </button> </template> @@ -178,6 +234,25 @@ export default { <gl-disclosure-dropdown-item :item="preferencesItem" data-testid="preferences-item" /> <gl-disclosure-dropdown-item + v-if="addBuyPipelineMinutesMenuItem" + ref="buyPipelineMinutesNotificationCallout" + :item="buyPipelineMinutesItem" + v-bind="buyPipelineMinutesCalloutData" + data-testid="buy-pipeline-minutes-item" + > + <template #list-item> + <span class="gl-display-flex gl-flex-direction-column"> + <span>{{ buyPipelineMinutesItem.text }} <gl-emoji data-name="clock9" /></span> + <span + v-if="data.pipeline_minutes.show_with_subtext" + class="gl-font-sm small gl-pt-2 gl-text-orange-800" + >{{ buyPipelineMinutesItem.warningText }}</span + > + </span> + </template> + </gl-disclosure-dropdown-item> + + <gl-disclosure-dropdown-item v-if="data.gitlab_com_but_not_canary" :item="gitlabNextItem" data-testid="gitlab-next-item" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index b78293a9815..028f5370028 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -313,7 +313,7 @@ export default { :status="statusIconName" :is-loading="isLoadingSummary" :class="{ 'gl-cursor-pointer': isCollapsible }" - class="gl-p-5" + class="gl-pl-5 gl-pr-4 gl-py-4" @mousedown="onRowMouseDown" @mouseup="onRowMouseUp" > @@ -381,7 +381,7 @@ export default { v-else-if="hasFullData" :items="fullData" :min-item-size="32" - class="report-block-container gl-px-5 gl-py-0" + class="report-block-container gl-p-0" > <template #default="{ item, index, active }"> <dynamic-scroller-item :item="item" :active="active" :class="{ active }"> @@ -389,7 +389,7 @@ export default { :class="{ 'gl-border-b-solid gl-border-b-1 gl-border-gray-100': index !== fullData.length - 1, }" - class="gl-py-3 gl-pl-7" + class="gl-py-3 gl-pl-9" data-testid="extension-list-item" > <gl-intersection-observer diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue index 73129a86877..a754d4e80ea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue @@ -287,7 +287,7 @@ export default { <template> <section class="media-section" data-testid="widget-extension"> - <div class="gl-p-5 gl-align-items-center gl-display-flex"> + <div class="gl-px-5 gl-py-4 gl-align-items-center gl-display-flex"> <status-icon :level="1" :name="widgetName" diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue index a001b6bdf24..23fbf211d54 100644 --- a/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue +++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue @@ -149,7 +149,7 @@ export default { > <slot> <button - class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-p-4 gl-mb-0" + class="card upload-dropzone-card upload-dropzone-border gl-w-full gl-h-full gl-align-items-center gl-justify-content-center gl-px-5 gl-py-4 gl-mb-0" type="button" @click="openFileUpload" > diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue index 355f17e970b..44c757f8f59 100644 --- a/app/assets/javascripts/work_items/components/widget_wrapper.vue +++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue @@ -44,7 +44,7 @@ export default { <template> <div class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-4"> <div - class="gl-px-5 gl-py-3 gl-display-flex gl-justify-content-space-between" + class="gl-pl-5 gl-pr-4 gl-py-4 gl-display-flex gl-justify-content-space-between gl-bg-white" :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }" > <div class="gl-display-flex gl-flex-grow-1"> diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss index 293caf6fc87..23bd2980c48 100644 --- a/app/assets/stylesheets/components/related_items_list.scss +++ b/app/assets/stylesheets/components/related_items_list.scss @@ -6,6 +6,8 @@ $item-remove-button-space: 42px; .related-items-list { padding: $gl-padding-4; padding-right: $gl-padding-6; + border-bottom-left-radius: $gl-border-size-3; + border-bottom-right-radius: $gl-border-size-3; &, .list-item:last-child { diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss index c15bc8d9895..dd723d9f4f4 100644 --- a/app/assets/stylesheets/framework/super_sidebar.scss +++ b/app/assets/stylesheets/framework/super_sidebar.scss @@ -11,6 +11,18 @@ } } +@mixin notification-dot($color, $size, $top, $left) { + background-color: $color; + border: 2px solid $gray-10; // Same as the sidebar's background color. + position: absolute; + height: $size; + width: $size; + top: $top; + left: $left; + border-radius: 50%; + transition: background-color 100ms linear, border-color 100ms linear; +} + .super-sidebar { @include gl-fixed; @include gl-top-0; @@ -98,16 +110,12 @@ .btn-with-notification { position: relative; - .notification { - background-color: $blue-500; - border: 2px solid $gray-10; // Same as the sidebar's background color. - position: absolute; - height: 9px; - width: 9px; - top: 5px; - left: 22px; - border-radius: 50%; - transition: background-color 100ms linear, border-color 100ms linear; + .notification-dot-info { + @include notification-dot($blue-500, 9px, 5px, 22px); + } + + .notification-dot-warning { + @include notification-dot($orange-300, 12px, 1px, 19px); } &:hover, diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index ac27f27ae2a..e032961a253 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -908,6 +908,11 @@ Compare Branches */ $compare-branches-sticky-header-height: 68px; +/* +Board Swimlanes +*/ +$board-swimlanes-headers-height: 64px; + /** Bootstrap 4.2.0 introduced new icons for validating forms. Our design system does not use those, so we are disabling them for now: diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss index a53601445ec..f4e515704fc 100644 --- a/app/assets/stylesheets/page_bundles/merge_requests.scss +++ b/app/assets/stylesheets/page_bundles/merge_requests.scss @@ -819,7 +819,7 @@ $tabs-holder-z-index: 250; .mr-widget-body, .mr-widget-content { - padding: $gl-padding; + padding: $gl-padding-12 $gl-padding; } .mr-widget-body-ready-merge { @@ -840,6 +840,11 @@ $tabs-holder-z-index: 250; } } +.mr-widget-grouped-section .report-block-container { + border-bottom-left-radius: $border-radius-default; + border-bottom-right-radius: $border-radius-default; +} + .mr-widget-extension { border-top: 1px solid var(--border-color, $border-color); background-color: var(--gray-10, $gray-10); @@ -916,7 +921,7 @@ $tabs-holder-z-index: 250; border-left: 2px solid var(--border-color, $border-color); position: absolute; bottom: -17px; - left: calc(1rem - 1px); + left: 26px; height: 16px; } } diff --git a/app/graphql/mutations/projects/sync_fork.rb b/app/graphql/mutations/projects/sync_fork.rb new file mode 100644 index 00000000000..bb92f078fae --- /dev/null +++ b/app/graphql/mutations/projects/sync_fork.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Mutations + module Projects + class SyncFork < BaseMutation + graphql_name 'ProjectSyncFork' + + include FindsProject + + authorize :push_code + + argument :project_path, GraphQL::Types::ID, + required: true, + description: 'Full path of the project to initialize.' + + argument :target_branch, GraphQL::Types::String, + required: true, + description: 'Ref of the fork to fetch into.' + + field :details, Types::Projects::ForkDetailsType, + null: true, + description: 'Updated fork details.' + + def resolve(project_path:, target_branch:) + project = authorized_find!(project_path) + details_resolver = Resolvers::Projects::ForkDetailsResolver.new(object: project, context: context, field: nil) + details = details_resolver.resolve(ref: target_branch) + + return respond(nil, ['This branch of this project cannot be updated from the upstream']) unless details + + enqueue_sync_fork(project, target_branch, details) + end + + def enqueue_sync_fork(project, target_branch, details) + return respond(details, []) if details.counts[:behind] == 0 + + if details.has_conflicts? + return respond(details, ['The synchronization cannot happen due to the merge conflict']) + end + + return respond(details, ['This service has been called too many times.']) if rate_limit_throttled?(project) + return respond(details, ['Another fork sync is already in progress']) unless details.exclusive_lease.try_obtain + + ::Projects::Forks::SyncWorker.perform_async(project.id, current_user.id, target_branch) # rubocop:disable CodeReuse/Worker + + respond(details, []) + end + + def rate_limit_throttled?(project) + Gitlab::ApplicationRateLimiter.throttled?(:project_fork_sync, scope: [project, current_user]) + end + + def respond(details, errors) + { details: details, errors: errors } + end + end + end +end diff --git a/app/graphql/resolvers/projects/fork_details_resolver.rb b/app/graphql/resolvers/projects/fork_details_resolver.rb index fcc13a1bc1e..a3c60f55e14 100644 --- a/app/graphql/resolvers/projects/fork_details_resolver.rb +++ b/app/graphql/resolvers/projects/fork_details_resolver.rb @@ -13,8 +13,17 @@ module Resolvers def resolve(**args) return unless project.forked? + return unless authorized_fork_source? + return unless project.repository.branch_exists?(args[:ref]) + return unless Feature.enabled?(:fork_divergence_counts, project) - ::Projects::Forks::DivergenceCounts.new(project, args[:ref]).counts + ::Projects::Forks::Details.new(project, args[:ref]) + end + + private + + def authorized_fork_source? + Ability.allowed?(current_user, :read_code, project.fork_source) end end end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index f72ad183fb0..d5bb35c1240 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -90,6 +90,7 @@ module Types mount_mutation Mutations::Notes::Update::ImageDiffNote mount_mutation Mutations::Notes::RepositionImageDiffNote mount_mutation Mutations::Notes::Destroy + mount_mutation Mutations::Projects::SyncFork, calls_gitaly: true, alpha: { milestone: '15.9' } mount_mutation Mutations::Releases::Create mount_mutation Mutations::Releases::Update mount_mutation Mutations::Releases::Delete diff --git a/app/graphql/types/projects/fork_details_type.rb b/app/graphql/types/projects/fork_details_type.rb index 88c17d89620..6157dc47255 100644 --- a/app/graphql/types/projects/fork_details_type.rb +++ b/app/graphql/types/projects/fork_details_type.rb @@ -9,11 +9,37 @@ module Types field :ahead, GraphQL::Types::Int, null: true, + calls_gitaly: true, + method: :ahead, description: 'Number of commits ahead of upstream.' field :behind, GraphQL::Types::Int, null: true, + calls_gitaly: true, + method: :behind, description: 'Number of commits behind upstream.' + + field :is_syncing, GraphQL::Types::Boolean, + null: true, + method: :syncing?, + description: 'Indicates if there is a synchronization in progress.' + + field :has_conflicts, GraphQL::Types::Boolean, + null: true, + method: :has_conflicts?, + description: 'Indicates if the fork conflicts with its upstream project.' + + def ahead + counts[:ahead] + end + + def behind + counts[:behind] + end + + def counts + @counts ||= object.counts + end end # rubocop: enable Graphql/AuthorizeTypes end diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index ad1aa3ad734..55a191d85b3 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -9,15 +9,27 @@ module NavHelper header_links.include?(link) end + def page_has_sidebar? + defined?(@left_sidebar) && @left_sidebar + end + + def page_has_collapsed_sidebar? + page_has_sidebar? && collapsed_sidebar? + end + + def page_has_collapsed_super_sidebar? + page_has_sidebar? && collapsed_super_sidebar? + end + def page_with_sidebar_class class_name = page_gutter_class if show_super_sidebar? - class_name << 'page-with-super-sidebar' if defined?(@left_sidebar) && @left_sidebar - class_name << 'page-with-super-sidebar-collapsed' if collapsed_super_sidebar? && @left_sidebar + class_name << 'page-with-super-sidebar' if page_has_sidebar? + class_name << 'page-with-super-sidebar-collapsed' if page_has_collapsed_super_sidebar? else - class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar - class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar + class_name << 'page-with-contextual-sidebar' if page_has_sidebar? + class_name << 'page-with-icon-sidebar' if page_has_collapsed_sidebar? end class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar diff --git a/app/helpers/users/callouts_helper.rb b/app/helpers/users/callouts_helper.rb index 2b8368dd29f..d4603afb727 100644 --- a/app/helpers/users/callouts_helper.rb +++ b/app/helpers/users/callouts_helper.rb @@ -11,6 +11,7 @@ module Users UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout' SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout' MERGE_REQUEST_SETTINGS_MOVED_CALLOUT = 'merge_request_settings_moved_callout' + PAGES_MOVED_CALLOUT = 'pages_moved_callout' REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze WEB_HOOK_DISABLED = 'web_hook_disabled' ULTIMATE_FEATURE_REMOVAL_BANNER = 'ultimate_feature_removal_banner' @@ -76,6 +77,10 @@ module Users !user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT) && project.merge_requests_enabled? end + def show_pages_menu_callout? + !user_dismissed?(PAGES_MOVED_CALLOUT) + end + def ultimate_feature_removal_banner_dismissed?(project) return false unless project diff --git a/app/models/projects/forks/divergence_counts.rb b/app/models/projects/forks/details.rb index 7d630b00083..9e09ef09022 100644 --- a/app/models/projects/forks/divergence_counts.rb +++ b/app/models/projects/forks/details.rb @@ -3,8 +3,11 @@ module Projects module Forks # Class for calculating the divergence of a fork with the source project - class DivergenceCounts + class Details + include Gitlab::Utils::StrongMemoize + LATEST_COMMITS_COUNT = 10 + LEASE_TIMEOUT = 15.minutes.to_i EXPIRATION_TIME = 8.hours def initialize(project, ref) @@ -20,32 +23,55 @@ module Projects { ahead: ahead, behind: behind } end + def exclusive_lease + key = ['project_details', project.id, ref].join(':') + uuid = Gitlab::ExclusiveLease.get_uuid(key) + + Gitlab::ExclusiveLease.new(key, uuid: uuid, timeout: LEASE_TIMEOUT) + end + strong_memoize_attr :exclusive_lease + + def syncing? + exclusive_lease.exists? + end + + def has_conflicts? + !(attrs && attrs[:has_conflicts]).nil? + end + + def update!(params) + Rails.cache.write(cache_key, params, expires_in: EXPIRATION_TIME) + + @attrs = nil + end + private attr_reader :project, :fork_repo, :source_repo, :ref def cache_key - @cache_key ||= ['project_forks', project.id, ref, 'divergence_counts'] + @cache_key ||= ['project_fork_details', project.id, ref].join(':') end def divergence_counts - fork_sha = fork_repo.commit(ref).sha - source_sha = source_repo.commit.sha + sha = fork_repo.commit(ref)&.sha + source_sha = source_repo.commit&.sha - cached_source_sha, cached_fork_sha, counts = Rails.cache.read(cache_key) - return counts if cached_source_sha == source_sha && cached_fork_sha == fork_sha + return if sha.blank? || source_sha.blank? - counts = calculate_divergence_counts(fork_sha, source_sha) + return attrs[:counts] if attrs.present? && attrs[:source_sha] == source_sha && attrs[:sha] == sha - Rails.cache.write(cache_key, [source_sha, fork_sha, counts], expires_in: EXPIRATION_TIME) + counts = calculate_divergence_counts(sha, source_sha) + + update!({ sha: sha, source_sha: source_sha, counts: counts }) counts end - def calculate_divergence_counts(fork_sha, source_sha) + def calculate_divergence_counts(sha, source_sha) # If the upstream latest commit exists in the fork repo, then # it's possible to calculate divergence counts within the fork repository. - return fork_repo.diverging_commit_count(fork_sha, source_sha) if fork_repo.commit(source_sha) + return fork_repo.diverging_commit_count(sha, source_sha) if fork_repo.commit(source_sha) # Otherwise, we need to find a commit that exists both in the fork and upstream # in order to use this commit as a base for calculating divergence counts. @@ -67,6 +93,10 @@ module Projects [ahead, behind] end + + def attrs + @attrs ||= Rails.cache.read(cache_key) + end end end end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index c9f414f3605..8898f7feb17 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -162,10 +162,7 @@ module Notes track_note_creation_usage_for_merge_requests(note) if note.for_merge_request? track_incident_action(user, note.noteable, 'incident_comment') if note.for_issue? track_note_creation_in_ipynb(note) - - if Feature.enabled?(:notes_create_service_tracking, project) - Gitlab::Tracking.event('Notes::CreateService', 'execute', **tracking_data_for(note)) - end + track_note_creation_visual_review(note) if Feature.enabled?(:route_hll_to_snowplow_phase4, project&.namespace) && note.for_commit? metric_key_path = 'counts.commit_comment' @@ -209,6 +206,10 @@ module Notes Gitlab::UsageDataCounters::IpynbDiffActivityCounter.note_created(note) end + + def track_note_creation_visual_review(note) + Gitlab::Tracking.event('Notes::CreateService', 'execute', **tracking_data_for(note)) + end end end diff --git a/app/services/projects/forks/sync_service.rb b/app/services/projects/forks/sync_service.rb new file mode 100644 index 00000000000..4c70d7f17f5 --- /dev/null +++ b/app/services/projects/forks/sync_service.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +module Projects + module Forks + # A service for fetching upstream default branch and merging it to the fork's specified branch. + class SyncService < BaseService + ONGOING_MERGE_ERROR = 'The synchronization did not happen due to another merge in progress' + + MergeError = Class.new(StandardError) + + def initialize(project, user, target_branch) + super(project, user) + + @source_project = project.fork_source + @head_sha = project.repository.commit(target_branch).sha + @target_branch = target_branch + @details = Projects::Forks::Details.new(project, target_branch) + end + + def execute + execute_service + + ServiceResponse.success + rescue MergeError => e + Gitlab::ErrorTracking.log_exception(e, { project_id: project.id, user_id: current_user.id }) + + ServiceResponse.error(message: e.message) + ensure + details.exclusive_lease.cancel + end + + private + + attr_reader :source_project, :head_sha, :target_branch, :details + + # The method executes multiple steps: + # + # 1. Gitlab::Git::CrossRepo fetches upstream default branch into a temporary ref and returns new source sha. + # 2. New divergence counts are calculated using the source sha. + # 3. If the fork is not behind, there is nothing to merge -> exit. + # 4. Otherwise, continue with the new source sha. + # 5. If Gitlab::Git::CommandError is raised it means that merge couldn't happen due to a merge conflict. The + # details are updated to transfer this error to the user. + def execute_service + counts = [] + source_sha = source_project.commit.sha + + Gitlab::Git::CrossRepo.new(repository, source_project.repository) + .execute(source_sha) do |cross_repo_source_sha| + counts = repository.diverging_commit_count(head_sha, cross_repo_source_sha) + ahead, behind = counts + next if behind == 0 + + execute_with_fetched_source(cross_repo_source_sha, ahead) + end + rescue Gitlab::Git::CommandError => e + details.update!({ sha: head_sha, source_sha: source_sha, counts: counts, has_conflicts: true }) + + raise MergeError, e.message + end + + def execute_with_fetched_source(cross_repo_source_sha, ahead) + with_linked_lfs_pointers(cross_repo_source_sha) do + merge_commit_id = perform_merge(cross_repo_source_sha, ahead) + raise MergeError, ONGOING_MERGE_ERROR unless merge_commit_id + end + end + + # This method merges the upstream default branch to the fork specified branch. + # Depending on whether the fork branch is ahead of upstream or not, a different type of + # merge is performed. + # + # If the fork's branch is not ahead of the upstream (only behind), fast-forward merge is performed. + # However, if the fork's branch contains commits that don't exist upstream, a merge commit is created. + # In this case, a conflict may happen, which interrupts the merge and returns a message to the user. + def perform_merge(cross_repo_source_sha, ahead) + if ahead > 0 + message = "Merge branch #{source_project.path}:#{source_project.default_branch} into #{target_branch}" + + repository.merge_to_branch(current_user, + source_sha: cross_repo_source_sha, + target_branch: target_branch, + target_sha: head_sha, + message: message) + else + repository.ff_merge(current_user, cross_repo_source_sha, target_branch, target_sha: head_sha) + end + end + + # This method links the newly merged lfs objects (if any) with the existing ones upstream. + # The LfsLinkService service has a limit and may raise an error if there are too many lfs objects to link. + # This is the reason why the block is passed: + # + # 1. Verify that there are not too many lfs objects to link + # 2. Execute the block (which basically performs the merge) + # 3. Link lfs objects + def with_linked_lfs_pointers(newrev, &block) + return yield unless project.lfs_enabled? + + oldrev = head_sha + new_lfs_oids = + Gitlab::Git::LfsChanges + .new(repository, newrev) + .new_pointers(not_in: [oldrev]) + .map(&:lfs_oid) + + Projects::LfsPointers::LfsLinkService.new(project).execute(new_lfs_oids, &block) + rescue Projects::LfsPointers::LfsLinkService::TooManyOidsError => e + raise MergeError, e.message + end + end + end +end diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb index cf3cc5cd8e0..f8f03d481af 100644 --- a/app/services/projects/lfs_pointers/lfs_link_service.rb +++ b/app/services/projects/lfs_pointers/lfs_link_service.rb @@ -15,9 +15,9 @@ module Projects def execute(oids) return [] unless project&.lfs_enabled? - if oids.size > MAX_OIDS - raise TooManyOidsError, 'Too many LFS object ids to link, please push them manually' - end + validate!(oids) + + yield if block_given? # Search and link existing LFS Object link_existing_lfs_objects(oids) @@ -25,6 +25,12 @@ module Projects private + def validate!(oids) + return if oids.size <= MAX_OIDS + + raise TooManyOidsError, 'Too many LFS object ids to link, please push them manually' + end + def link_existing_lfs_objects(oids) linked_existing_objects = [] iterations = 0 diff --git a/app/views/admin/runners/register.html.haml b/app/views/admin/runners/register.html.haml index f1477d38e98..662bb9ea00e 100644 --- a/app/views/admin/runners/register.html.haml +++ b/app/views/admin/runners/register.html.haml @@ -1,4 +1,7 @@ -- add_to_breadcrumbs _('Runners'), admin_runners_path +- runner_name = "##{@runner.id} (#{@runner.short_sha})" - breadcrumb_title s_('Runners|Register') - page_title s_('Runners|Register'), "##{@runner.id} (#{@runner.short_sha})" +- add_to_breadcrumbs _('Runners'), admin_runners_path +- add_to_breadcrumbs runner_name, register_admin_runner_path(@runner) +#js-admin-register-runner{ data: { runner_id: @runner.id, runners_path: admin_runners_path } } diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index e87005434e4..b2270e0faf7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -7,6 +7,13 @@ = render_if_exists 'shared/ultimate_feature_removal_banner', project: @project +- if Feature.enabled?(:show_pages_in_deployments_menu, current_user, type: :experiment) + = render Pajamas::AlertComponent.new(variant: :info, + title: _('GitLab Pages has moved'), + alert_options: { class: 'gl-my-5', data: { feature_id: Users::CalloutsHelper::PAGES_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c| + = c.body do + = _('To go to GitLab Pages, on the left sidebar, select %{pages_link}.').html_safe % {pages_link: link_to('Deployments > Pages', project_pages_path(@project)).html_safe} + %section.settings.general-settings.no-animate.expanded#js-general-settings .settings-header %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Naming, topics, avatar') @@ -27,7 +34,6 @@ %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' } %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe .js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) } - - if show_merge_request_settings_callout?(@project) %section.settings.expanded = render Pajamas::AlertComponent.new(variant: :info, diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 21946c0e52b..fbb348811e0 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -3108,6 +3108,15 @@ :weight: 1 :idempotent: true :tags: [] +- :name: projects_forks_sync + :worker_name: Projects::Forks::SyncWorker + :feature_category: :source_code_management + :has_external_dependencies: false + :urgency: :high + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: projects_git_garbage_collect :worker_name: Projects::GitGarbageCollectWorker :feature_category: :gitaly diff --git a/app/workers/projects/forks/sync_worker.rb b/app/workers/projects/forks/sync_worker.rb new file mode 100644 index 00000000000..2fa6785bc91 --- /dev/null +++ b/app/workers/projects/forks/sync_worker.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Projects + module Forks + class SyncWorker + include ApplicationWorker + + data_consistency :sticky + idempotent! + urgency :high + feature_category :source_code_management + + def perform(project_id, user_id, ref) + project = Project.find_by_id(project_id) + user = User.find_by_id(user_id) + return unless project && user + + ::Projects::Forks::SyncService.new(project, user, ref).execute + end + end + end +end |