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
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-28 21:08:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-28 21:08:32 +0300
commit36eff6e5089629619cc55f4771fa949d6ae2b29b (patch)
tree6381b0c90f403c535abdde2f712cd346a78770fe /app
parentbaed745d21710f1d78ece03558873acd6fd7d358 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js4
-rw-r--r--app/assets/javascripts/ci/runner/admin_register_runner/admin_register_runner_app.vue81
-rw-r--r--app/assets/javascripts/ci/runner/admin_register_runner/index.js33
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/cli_command.vue42
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/registration_instructions.vue142
-rw-r--r--app/assets/javascripts/ci/runner/components/registration/utils.js43
-rw-r--r--app/assets/javascripts/ci/runner/constants.js4
-rw-r--r--app/assets/javascripts/ci/runner/graphql/register/runner_for_registration.query.graphql7
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/components/related_merge_requests.vue6
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_block.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/help_center.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue99
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/upload_dropzone.vue2
-rw-r--r--app/assets/javascripts/work_items/components/widget_wrapper.vue2
-rw-r--r--app/assets/stylesheets/components/related_items_list.scss2
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss28
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss9
-rw-r--r--app/graphql/mutations/projects/sync_fork.rb58
-rw-r--r--app/graphql/resolvers/projects/fork_details_resolver.rb11
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/graphql/types/projects/fork_details_type.rb26
-rw-r--r--app/helpers/nav_helper.rb20
-rw-r--r--app/helpers/users/callouts_helper.rb5
-rw-r--r--app/models/projects/forks/details.rb (renamed from app/models/projects/forks/divergence_counts.rb)50
-rw-r--r--app/services/notes/create_service.rb9
-rw-r--r--app/services/projects/forks/sync_service.rb113
-rw-r--r--app/services/projects/lfs_pointers/lfs_link_service.rb12
-rw-r--r--app/views/admin/runners/register.html.haml5
-rw-r--r--app/views/projects/edit.html.haml8
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/projects/forks/sync_worker.rb22
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