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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-09 18:11:41 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-09 18:11:41 +0300
commitd56569ff3e73ae1dbcf93d2530925c4ecb8fd185 (patch)
treef89e6dd59d8d807201a9dd3ca46b5eee0ea5f438
parent1faea1c6a0464e44dca4477fb31846938c2ad871 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml9
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/custom_emoji/components/delete_item.vue90
-rw-r--r--app/assets/javascripts/custom_emoji/components/list.vue10
-rw-r--r--app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql3
-rw-r--r--app/assets/javascripts/custom_emoji/queries/delete_custom_emoji.mutation.graphql7
-rw-r--r--app/assets/javascripts/super_sidebar/components/brand_logo.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/create_menu.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue12
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue196
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue (renamed from app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue)0
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue (renamed from app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue)0
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue130
-rw-r--r--app/helpers/time_helper.rb24
-rw-r--r--app/services/merge_requests/create_ref_service.rb130
-rw-r--r--config/feature_flags/development/merge_trains_create_ref_service.yml8
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--db/migrate/20230804133028_add_index_custom_email_verifications_on_triggered_at_and_state_started.rb17
-rw-r--r--db/schema_migrations/202308041330281
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/raketasks/maintenance.md41
-rw-r--r--doc/api/graphql/reference/index.md6
-rw-r--r--doc/api/settings.md8
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/workspace/configuration.md68
-rw-r--r--doc/user/workspace/index.md85
-rw-r--r--lib/gitlab/auth.rb6
-rw-r--r--lib/gitlab/blame.rb4
-rw-r--r--lib/gitlab/git/blame.rb18
-rw-r--r--lib/gitlab/git/repository.rb12
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb20
-rw-r--r--lib/tasks/gitlab/info.rake13
-rw-r--r--locale/gitlab.pot11
-rw-r--r--spec/frontend/custom_emoji/components/delete_item_spec.js89
-rw-r--r--spec/frontend/custom_emoji/components/list_spec.js18
-rw-r--r--spec/frontend/custom_emoji/mock_data.js3
-rw-r--r--spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js179
-rw-r--r--spec/frontend/work_items/components/shared/work_item_link_child_metadata_spec.js (renamed from spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js)2
-rw-r--r--spec/frontend/work_items/components/shared/work_item_links_menu_spec.js (renamed from spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js)2
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js169
-rw-r--r--spec/helpers/time_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/blame_spec.rb6
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb8
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb33
-rw-r--r--spec/services/merge_requests/create_ref_service_spec.rb164
47 files changed, 1238 insertions, 383 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 09d6274e045..f103032ee69 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -31,6 +31,10 @@
- assets_compile_script
- echo -n "${GITLAB_ASSETS_HASH}" > "cached-assets-hash.txt"
+.update-cache-base:
+ after_script:
+ - yarn patch-package --reverse # To avoid caching patched modules
+
compile-production-assets:
extends:
- .compile-assets-base
@@ -47,8 +51,6 @@ compile-production-assets:
- public/assets/
- "${WEBPACK_COMPILE_LOG_PATH}"
when: always
- after_script:
- - rm -f /etc/apt/sources.list.d/google*.list # We don't need to update Chrome here
compile-production-assets as-if-foss:
extends:
@@ -77,6 +79,7 @@ compile-test-assets as-if-foss:
update-assets-compile-production-cache:
extends:
- compile-production-assets
+ - .update-cache-base
- .assets-compile-cache-push
- .shared:rules:update-cache
stage: prepare
@@ -85,6 +88,7 @@ update-assets-compile-production-cache:
update-assets-compile-test-cache:
extends:
- compile-test-assets
+ - .update-cache-base
- .assets-compile-cache-push
- .shared:rules:update-cache
stage: prepare
@@ -94,6 +98,7 @@ update-storybook-yarn-cache:
extends:
- .default-retry
- .default-utils-before_script
+ - .update-cache-base
- .storybook-yarn-cache-push
- .shared:rules:update-cache
stage: prepare
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 26f450f5dc6..52365968613 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0ec311c007a78701fa4ee2ed8d58ca686378fcf0
+38aa3f1b70406c72e9ab26389cbdecbdd3218a0a
diff --git a/app/assets/javascripts/custom_emoji/components/delete_item.vue b/app/assets/javascripts/custom_emoji/components/delete_item.vue
new file mode 100644
index 00000000000..9d13d40dc47
--- /dev/null
+++ b/app/assets/javascripts/custom_emoji/components/delete_item.vue
@@ -0,0 +1,90 @@
+<script>
+import * as Sentry from '@sentry/browser';
+import { uniqueId } from 'lodash';
+import { GlButton, GlTooltipDirective, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { createAlert } from '~/alert';
+import { __ } from '~/locale';
+import deleteCustomEmojiMutation from '../queries/delete_custom_emoji.mutation.graphql';
+
+export default {
+ name: 'DeleteItem',
+ components: {
+ GlButton,
+ GlModal,
+ GlSprintf,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlModal: GlModalDirective,
+ },
+ props: {
+ emoji: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isDeleting: false,
+ modalId: uniqueId('delete-custom-emoji-'),
+ };
+ },
+ methods: {
+ showModal() {
+ this.$refs['delete-modal'].show();
+ },
+ async onDelete() {
+ this.isDeleting = true;
+
+ try {
+ await this.$apollo.mutate({
+ mutation: deleteCustomEmojiMutation,
+ variables: {
+ id: this.emoji.id,
+ },
+ update: (cache) => {
+ const cacheId = cache.identify(this.emoji);
+ cache.evict({ id: cacheId });
+ },
+ });
+ } catch (e) {
+ createAlert(__('Failed to delete custom emoji. Please try again.'));
+ Sentry.captureException(e);
+ }
+ },
+ },
+ actionPrimary: { text: __('Delete'), attributes: { variant: 'danger' } },
+ actionSecondary: { text: __('Cancel'), attributes: { variant: 'default' } },
+};
+</script>
+
+<template>
+ <div>
+ <gl-button
+ v-gl-tooltip
+ icon="remove"
+ :aria-label="__('Delete custom emoji')"
+ :title="__('Delete custom emoji')"
+ :loading="isDeleting"
+ data-testid="delete-button"
+ @click="showModal"
+ />
+ <gl-modal
+ ref="delete-modal"
+ :title="__('Delete custom emoji')"
+ :action-primary="$options.actionPrimary"
+ :action-secondary="$options.actionSecondary"
+ :modal-id="modalId"
+ size="sm"
+ @primary="onDelete"
+ >
+ <gl-sprintf
+ :message="__('Are you sure you want to delete %{name}? This action cannot be undone.')"
+ >
+ <template #name
+ ><strong>{{ emoji.name }}</strong></template
+ >
+ </gl-sprintf>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/custom_emoji/components/list.vue b/app/assets/javascripts/custom_emoji/components/list.vue
index 03ed2ffe2ec..72b28e8db4a 100644
--- a/app/assets/javascripts/custom_emoji/components/list.vue
+++ b/app/assets/javascripts/custom_emoji/components/list.vue
@@ -3,6 +3,7 @@
import { GlLoadingIcon, GlTableLite, GlTabs, GlTab, GlBadge, GlKeysetPagination } from '@gitlab/ui';
import { __ } from '~/locale';
import { formatDate } from '~/lib/utils/datetime/date_format_utility';
+import DeleteItem from './delete_item.vue';
export default {
components: {
@@ -12,6 +13,7 @@ export default {
GlTab,
GlBadge,
GlKeysetPagination,
+ DeleteItem,
},
props: {
loading: {
@@ -124,7 +126,13 @@ export default {
data-unicode-version="custom"
/>
</template>
- <template #cell(action)> </template>
+ <template #cell(action)="data">
+ <delete-item
+ v-if="data.item.userPermissions.deleteCustomEmoji"
+ :key="data.item.name"
+ :emoji="data.item"
+ />
+ </template>
<template #cell(created_at)="data">
{{ formatDate(data.item.createdAt) }}
</template>
diff --git a/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql b/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql
index 78e10ed93f8..a4189f80436 100644
--- a/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql
+++ b/app/assets/javascripts/custom_emoji/queries/custom_emojis.query.graphql
@@ -16,6 +16,9 @@ query getCustomEmojis($groupPath: ID!, $after: String = "", $before: String = ""
name
url
createdAt
+ userPermissions {
+ deleteCustomEmoji
+ }
}
}
}
diff --git a/app/assets/javascripts/custom_emoji/queries/delete_custom_emoji.mutation.graphql b/app/assets/javascripts/custom_emoji/queries/delete_custom_emoji.mutation.graphql
new file mode 100644
index 00000000000..37618bc2749
--- /dev/null
+++ b/app/assets/javascripts/custom_emoji/queries/delete_custom_emoji.mutation.graphql
@@ -0,0 +1,7 @@
+mutation deleteCustomEmoji($id: CustomEmojiID!) {
+ destroyCustomEmoji(input: { id: $id }) {
+ customEmoji {
+ id
+ }
+ }
+}
diff --git a/app/assets/javascripts/super_sidebar/components/brand_logo.vue b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
index 66381e4da4d..1589f4978e1 100644
--- a/app/assets/javascripts/super_sidebar/components/brand_logo.vue
+++ b/app/assets/javascripts/super_sidebar/components/brand_logo.vue
@@ -26,7 +26,7 @@ export default {
<template>
<a
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.homepage"
+ v-gl-tooltip:super-sidebar.hover.noninteractive.bottom.ds500="$options.i18n.homepage"
class="brand-logo"
:href="rootPath"
:title="$options.i18n.homepage"
diff --git a/app/assets/javascripts/super_sidebar/components/create_menu.vue b/app/assets/javascripts/super_sidebar/components/create_menu.vue
index 82f4fd18e80..3645606515f 100644
--- a/app/assets/javascripts/super_sidebar/components/create_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/create_menu.vue
@@ -95,6 +95,7 @@ export default {
:target="`#${$options.toggleId}`"
placement="bottom"
container="#super-sidebar"
+ noninteractive
>
{{ $options.i18n.createNew }}
</gl-tooltip>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 79d476ee74b..25d2b8a73ed 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -168,7 +168,7 @@ export default {
</gl-badge>
<gl-button
v-if="isPinnable && !isPinned"
- v-gl-tooltip.right.viewport="$options.i18n.pinItem"
+ v-gl-tooltip.noninteractive.ds500.right.viewport="$options.i18n.pinItem"
size="small"
category="tertiary"
icon="thumbtack"
@@ -177,7 +177,7 @@ export default {
/>
<gl-button
v-else-if="isPinnable && isPinned"
- v-gl-tooltip.right.viewport="$options.i18n.unpinItem"
+ v-gl-tooltip.noninteractive.ds500.right.viewport="$options.i18n.unpinItem"
size="small"
category="tertiary"
:aria-label="$options.i18n.unpinItem"
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
index 87762a62c0f..7d5e87805d5 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar_toggle.vue
@@ -74,7 +74,7 @@ export default {
<template>
<gl-button
- v-gl-tooltip.hover="tooltip"
+ v-gl-tooltip.hover.noninteractive.ds500="tooltip"
aria-controls="super-sidebar"
:aria-expanded="ariaExpanded"
:aria-label="$options.i18n.navigationSidebar"
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 0188585df40..5bc8ad97cd5 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -130,7 +130,7 @@ export default {
<gl-button
id="super-sidebar-search"
- v-gl-tooltip.bottom.hover.html="searchTooltip"
+ v-gl-tooltip.bottom.hover.noninteractive.ds500.html="searchTooltip"
v-gl-modal="$options.SEARCH_MODAL_ID"
data-testid="super-sidebar-search-button"
icon="search"
@@ -143,7 +143,7 @@ export default {
<gl-button
v-if="isImpersonating"
- v-gl-tooltip
+ v-gl-tooltip.noninteractive.ds500.bottom
:href="sidebarData.stop_impersonation_path"
:title="$options.i18n.stopImpersonating"
:aria-label="$options.i18n.stopImpersonating"
@@ -159,7 +159,7 @@ export default {
class="gl-display-flex gl-justify-content-space-between gl-px-3 gl-py-2 gl-gap-2"
>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.issues"
+ v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="$options.i18n.issues"
class="gl-flex-basis-third dashboard-shortcuts-issues"
icon="issues"
:count="userCounts.assigned_issues"
@@ -177,7 +177,9 @@ export default {
@hidden="mrMenuShown = false"
>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="mrMenuShown ? '' : $options.i18n.mergeRequests"
+ v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="
+ mrMenuShown ? '' : $options.i18n.mergeRequests
+ "
class="gl-w-full"
icon="merge-request-open"
:count="mergeRequestTotalCount"
@@ -189,7 +191,7 @@ export default {
/>
</merge-request-menu>
<counter
- v-gl-tooltip:super-sidebar.hover.bottom="$options.i18n.todoList"
+ v-gl-tooltip:super-sidebar.hover.noninteractive.ds500.bottom="$options.i18n.todoList"
class="gl-flex-basis-third shortcuts-todos js-todos-count"
icon="todo-done"
:count="userCounts.todos"
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
new file mode 100644
index 00000000000..0a38dcb77f6
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -0,0 +1,196 @@
+<script>
+import { GlLabel, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue';
+import {
+ STATE_OPEN,
+ TASK_TYPE_NAME,
+ WIDGET_TYPE_PROGRESS,
+ WIDGET_TYPE_HIERARCHY,
+ WIDGET_TYPE_HEALTH_STATUS,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_LABELS,
+ WORK_ITEM_NAME_TO_ICON_MAP,
+} from '../../constants';
+import WorkItemLinksMenu from './work_item_links_menu.vue';
+
+export default {
+ i18n: {
+ confidential: __('Confidential'),
+ created: __('Created'),
+ closed: __('Closed'),
+ },
+ components: {
+ GlLabel,
+ GlLink,
+ GlIcon,
+ RichTimestampTooltip,
+ WorkItemLinkChildMetadata,
+ WorkItemLinksMenu,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ childItem: {
+ type: Object,
+ required: true,
+ },
+ canUpdate: {
+ type: Boolean,
+ required: true,
+ },
+ parentWorkItemId: {
+ type: String,
+ required: true,
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ childPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ labels() {
+ return this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes || [];
+ },
+ metadataWidgets() {
+ return this.childItem.widgets?.reduce((metadataWidgets, widget) => {
+ // Skip Hierarchy widget as it is not part of metadata.
+ if (widget.type && widget.type !== WIDGET_TYPE_HIERARCHY) {
+ // eslint-disable-next-line no-param-reassign
+ metadataWidgets[widget.type] = widget;
+ }
+ return metadataWidgets;
+ }, {});
+ },
+ allowsScopedLabels() {
+ return this.metadataWidgets[WIDGET_TYPE_LABELS]?.allowsScopedLabels;
+ },
+ isChildItemOpen() {
+ return this.childItem.state === STATE_OPEN;
+ },
+ iconName() {
+ if (this.childItemType === TASK_TYPE_NAME) {
+ return this.isChildItemOpen ? 'issue-open-m' : 'issue-close';
+ }
+ return WORK_ITEM_NAME_TO_ICON_MAP[this.childItemType];
+ },
+ childItemType() {
+ return this.childItem.workItemType.name;
+ },
+ iconClass() {
+ if (this.childItemType === TASK_TYPE_NAME) {
+ return this.isChildItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
+ }
+ return '';
+ },
+ stateTimestamp() {
+ return this.isChildItemOpen ? this.childItem.createdAt : this.childItem.closedAt;
+ },
+ stateTimestampTypeText() {
+ return this.isChildItemOpen ? this.$options.i18n.created : this.$options.i18n.closed;
+ },
+ hasMetadata() {
+ if (this.metadataWidgets) {
+ return (
+ Number.isInteger(this.metadataWidgets[WIDGET_TYPE_PROGRESS]?.progress) ||
+ Boolean(this.metadataWidgets[WIDGET_TYPE_HEALTH_STATUS]?.healthStatus) ||
+ Boolean(this.metadataWidgets[WIDGET_TYPE_MILESTONE]?.milestone) ||
+ this.metadataWidgets[WIDGET_TYPE_ASSIGNEES]?.assignees?.nodes.length > 0 ||
+ this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes.length > 0
+ );
+ }
+ return false;
+ },
+ },
+ methods: {
+ showScopedLabel(label) {
+ return isScopedLabel(label) && this.allowsScopedLabels;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base"
+ data-testid="links-child"
+ >
+ <div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
+ <div
+ class="gl-display-flex gl-flex-grow-1 gl-flex-wrap flex-xl-nowrap gl-align-items-center gl-justify-content-space-between gl-gap-3 gl-min-w-0"
+ >
+ <div class="item-title gl-display-flex gl-gap-3 gl-min-w-0">
+ <span
+ :id="`stateIcon-${childItem.id}`"
+ class="gl-cursor-help"
+ data-testid="item-status-icon"
+ >
+ <gl-icon
+ class="gl-text-secondary"
+ :class="iconClass"
+ :name="iconName"
+ :aria-label="stateTimestampTypeText"
+ />
+ </span>
+ <rich-timestamp-tooltip
+ :target="`stateIcon-${childItem.id}`"
+ :raw-timestamp="stateTimestamp"
+ :timestamp-type-text="stateTimestampTypeText"
+ />
+ <span v-if="childItem.confidential">
+ <gl-icon
+ v-gl-tooltip.top
+ name="eye-slash"
+ class="gl-text-orange-500"
+ data-testid="confidential-icon"
+ :aria-label="$options.i18n.confidential"
+ :title="$options.i18n.confidential"
+ />
+ </span>
+ <gl-link
+ :href="childPath"
+ class="gl-text-truncate gl-text-black-normal! gl-font-weight-semibold"
+ data-testid="item-title"
+ @click="$emit('click', $event)"
+ @mouseover="$emit('mouseover')"
+ @mouseout="$emit('mouseout')"
+ >
+ {{ childItem.title }}
+ </gl-link>
+ </div>
+ <work-item-link-child-metadata
+ v-if="hasMetadata"
+ :metadata-widgets="metadataWidgets"
+ class="gl-ml-6 ml-xl-0"
+ />
+ </div>
+ <div v-if="labels.length" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
+ <gl-label
+ v-for="label in labels"
+ :key="label.id"
+ :title="label.title"
+ :background-color="label.color"
+ :description="label.description"
+ :scoped="showScopedLabel(label)"
+ class="gl-my-2 gl-mr-2 gl-mb-auto gl-label-sm"
+ tooltip-placement="top"
+ />
+ </div>
+ </div>
+ <div v-if="canUpdate" class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex">
+ <work-item-links-menu
+ data-testid="links-menu"
+ @removeChild="$emit('removeChild', childItem)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
index ddeac2b92ae..ddeac2b92ae 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_metadata.vue
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
index 53e8eedf060..53e8eedf060 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_links_menu.vue
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index ec44a654e89..a9b0c2b98bf 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -1,39 +1,27 @@
<script>
-import { GlButton, GlLabel, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import * as Sentry from '@sentry/browser';
import { __, s__ } from '~/locale';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { createAlert } from '~/alert';
-import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
-import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import {
STATE_OPEN,
TASK_TYPE_NAME,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
- WIDGET_TYPE_PROGRESS,
- WIDGET_TYPE_HEALTH_STATUS,
- WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_HIERARCHY,
- WIDGET_TYPE_ASSIGNEES,
- WIDGET_TYPE_LABELS,
WORK_ITEM_NAME_TO_ICON_MAP,
} from '../../constants';
import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
-import WorkItemLinksMenu from './work_item_links_menu.vue';
+import WorkItemLinkChildContents from '../shared/work_item_link_child_contents.vue';
import WorkItemTreeChildren from './work_item_tree_children.vue';
export default {
components: {
- GlLabel,
- GlLink,
GlButton,
- GlIcon,
- RichTimestampTooltip,
- WorkItemLinkChildMetadata,
- WorkItemLinksMenu,
WorkItemTreeChildren,
+ WorkItemLinkChildContents,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -74,25 +62,9 @@ export default {
};
},
computed: {
- labels() {
- return this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes || [];
- },
- allowsScopedLabels() {
- return this.metadataWidgets[WIDGET_TYPE_LABELS]?.allowsScopedLabels;
- },
canHaveChildren() {
return this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE;
},
- metadataWidgets() {
- return this.childItem.widgets?.reduce((metadataWidgets, widget) => {
- // Skip Hierarchy widget as it is not part of metadata.
- if (widget.type && widget.type !== WIDGET_TYPE_HIERARCHY) {
- // eslint-disable-next-line no-param-reassign
- metadataWidgets[widget.type] = widget;
- }
- return metadataWidgets;
- }, {});
- },
isItemOpen() {
return this.childItem.state === STATE_OPEN;
},
@@ -126,18 +98,6 @@ export default {
chevronTooltip() {
return this.isExpanded ? __('Collapse') : __('Expand');
},
- hasMetadata() {
- if (this.metadataWidgets) {
- return (
- Number.isInteger(this.metadataWidgets[WIDGET_TYPE_PROGRESS]?.progress) ||
- Boolean(this.metadataWidgets[WIDGET_TYPE_HEALTH_STATUS]?.healthStatus) ||
- Boolean(this.metadataWidgets[WIDGET_TYPE_MILESTONE]?.milestone) ||
- this.metadataWidgets[WIDGET_TYPE_ASSIGNEES]?.assignees?.nodes.length > 0 ||
- this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes.length > 0
- );
- }
- return false;
- },
},
watch: {
childItem: {
@@ -270,81 +230,15 @@ export default {
data-testid="expand-child"
@click="toggleItem"
/>
- <div
- class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base"
- data-testid="links-child"
- >
- <div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-wrap flex-xl-nowrap gl-align-items-center gl-justify-content-space-between gl-gap-3 gl-min-w-0"
- >
- <div class="item-title gl-display-flex gl-gap-3 gl-min-w-0">
- <span
- :id="`stateIcon-${childItem.id}`"
- class="gl-cursor-help"
- data-testid="item-status-icon"
- >
- <gl-icon
- class="gl-text-secondary"
- :class="iconClass"
- :name="iconName"
- :aria-label="stateTimestampTypeText"
- />
- </span>
- <rich-timestamp-tooltip
- :target="`stateIcon-${childItem.id}`"
- :raw-timestamp="stateTimestamp"
- :timestamp-type-text="stateTimestampTypeText"
- />
- <span v-if="childItem.confidential">
- <gl-icon
- v-gl-tooltip.top
- name="eye-slash"
- class="gl-text-orange-500"
- data-testid="confidential-icon"
- :aria-label="__('Confidential')"
- :title="__('Confidential')"
- />
- </span>
- <gl-link
- :href="childPath"
- class="gl-text-truncate gl-text-black-normal! gl-font-weight-semibold"
- data-testid="item-title"
- @click="$emit('click', $event)"
- @mouseover="$emit('mouseover')"
- @mouseout="$emit('mouseout')"
- >
- {{ childItem.title }}
- </gl-link>
- </div>
- <work-item-link-child-metadata
- v-if="hasMetadata"
- :metadata-widgets="metadataWidgets"
- class="gl-ml-6 ml-xl-0"
- />
- </div>
- <div v-if="labels.length" class="gl-display-flex gl-flex-wrap gl-flex-basis-full gl-ml-6">
- <gl-label
- v-for="label in labels"
- :key="label.id"
- :title="label.title"
- :background-color="label.color"
- :description="label.description"
- :scoped="showScopedLabel(label)"
- class="gl-my-2 gl-mr-2 gl-mb-auto gl-label-sm"
- tooltip-placement="top"
- />
- </div>
- </div>
- <div v-if="canUpdate" class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex">
- <work-item-links-menu
- :work-item-id="childItem.id"
- :parent-work-item-id="issuableGid"
- data-testid="links-menu"
- @removeChild="$emit('removeChild', childItem)"
- />
- </div>
- </div>
+ <work-item-link-child-contents
+ :child-item="childItem"
+ :can-update="canUpdate"
+ :parent-work-item-id="issuableGid"
+ :work-item-type="workItemType"
+ :child-path="childPath"
+ @click="$emit('click', $event)"
+ @removeChild="$emit('removeChild', childItem)"
+ />
</div>
<work-item-tree-children
v-if="isExpanded"
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index ad473875a53..0a5751c5221 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -1,20 +1,20 @@
# frozen_string_literal: true
module TimeHelper
+ TIME_UNIT_TRANSLATION = {
+ seconds: ->(seconds) { n_('%d second', '%d seconds', seconds) % seconds },
+ minutes: ->(minutes) { n_('%d minute', '%d minutes', minutes) % minutes },
+ hours: ->(hours) { n_('%d hour', '%d hours', hours) % hours },
+ days: ->(days) { n_('%d day', '%d days', days) % days },
+ weeks: ->(weeks) { n_('%d week', '%d weeks', weeks) % weeks },
+ months: ->(months) { n_('%d month', '%d months', months) % months },
+ years: ->(years) { n_('%d year', '%d years', years) % years }
+ }.freeze
+
def time_interval_in_words(interval_in_seconds)
- interval_in_seconds = interval_in_seconds.to_i
- minutes = interval_in_seconds / 60
- seconds = interval_in_seconds - minutes * 60
+ time_parts = ActiveSupport::Duration.build(interval_in_seconds.to_i).parts
- if minutes >= 1
- if seconds % 60 == 0
- n_('%d minute', '%d minutes', minutes) % minutes
- else
- [n_('%d minute', '%d minutes', minutes) % minutes, n_('%d second', '%d seconds', seconds) % seconds].to_sentence
- end
- else
- n_('%d second', '%d seconds', seconds) % seconds
- end
+ time_parts.map { |unit, value| TIME_UNIT_TRANSLATION[unit].call(value) }.to_sentence
end
def duration_in_numbers(duration_in_seconds)
diff --git a/app/services/merge_requests/create_ref_service.rb b/app/services/merge_requests/create_ref_service.rb
new file mode 100644
index 00000000000..7aa27e57405
--- /dev/null
+++ b/app/services/merge_requests/create_ref_service.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ # CreateRefService creates or overwrites a ref under "refs/merge-requests/"
+ # with a commit for the merged result.
+ class CreateRefService
+ include Gitlab::Utils::StrongMemoize
+
+ CreateRefError = Class.new(StandardError)
+
+ def initialize(
+ current_user:, merge_request:, target_ref:, first_parent_ref:,
+ source_sha: nil, merge_commit_message: nil)
+
+ @current_user = current_user
+ @merge_request = merge_request
+ @initial_source_sha = source_sha
+ @target_ref = target_ref
+ @merge_commit_message = merge_commit_message
+ @first_parent_sha = target_project.commit(first_parent_ref)&.sha
+ end
+
+ def execute
+ commit_sha = initial_source_sha # the SHA to be at HEAD of target_ref
+ source_sha = initial_source_sha # the SHA to be the merged result of the source (minus the merge commit)
+ expected_old_oid = "" # the SHA we expect target_ref to be at prior to an update (an optimistic lock)
+
+ # TODO: Update this message with the removal of FF merge_trains_create_ref_service and update tests
+ # This is for compatibility with MergeToRefService during the rollout.
+ return ServiceResponse.error(message: '3:Invalid merge source') unless first_parent_sha.present?
+
+ commit_sha, source_sha, expected_old_oid = maybe_squash!(commit_sha, source_sha, expected_old_oid)
+ commit_sha, source_sha, expected_old_oid = maybe_rebase!(commit_sha, source_sha, expected_old_oid)
+ commit_sha, source_sha = maybe_merge!(commit_sha, source_sha, expected_old_oid)
+
+ ServiceResponse.success(
+ payload: {
+ commit_sha: commit_sha,
+ target_sha: first_parent_sha,
+ source_sha: source_sha
+ }
+ )
+ rescue CreateRefError => error
+ ServiceResponse.error(message: error.message)
+ end
+
+ private
+
+ attr_reader :current_user, :merge_request, :target_ref, :first_parent_sha, :initial_source_sha
+
+ delegate :target_project, to: :merge_request
+ delegate :repository, to: :target_project
+
+ def maybe_squash!(commit_sha, source_sha, expected_old_oid)
+ if merge_request.squash_on_merge?
+ squash_result = MergeRequests::SquashService.new(
+ merge_request: merge_request,
+ current_user: current_user,
+ commit_message: squash_commit_message
+ ).execute
+ raise CreateRefError, squash_result[:message] if squash_result[:status] == :error
+
+ commit_sha = squash_result[:squash_sha]
+ source_sha = commit_sha
+ end
+
+ # squash does not overwrite target_ref, so expected_old_oid remains the same
+ [commit_sha, source_sha, expected_old_oid]
+ end
+
+ def maybe_rebase!(commit_sha, source_sha, expected_old_oid)
+ if target_project.ff_merge_must_be_possible?
+ commit_sha = safe_gitaly_operation do
+ repository.rebase_to_ref(
+ current_user,
+ source_sha: source_sha,
+ target_ref: target_ref,
+ first_parent_ref: first_parent_sha
+ )
+ end
+
+ source_sha = commit_sha
+ expected_old_oid = commit_sha
+ end
+
+ [commit_sha, source_sha, expected_old_oid]
+ end
+
+ def maybe_merge!(commit_sha, source_sha, expected_old_oid)
+ unless target_project.merge_requests_ff_only_enabled
+ source_sha = safe_gitaly_operation do
+ repository.merge_to_ref(
+ current_user,
+ source_sha: source_sha,
+ target_ref: target_ref,
+ message: merge_commit_message,
+ first_parent_ref: first_parent_sha,
+ branch: nil,
+ expected_old_oid: expected_old_oid
+ )
+ end
+ commit = target_project.commit(commit_sha)
+ _, source_sha = commit.parent_ids
+ end
+
+ [commit_sha, source_sha]
+ end
+
+ def safe_gitaly_operation
+ yield
+ rescue Gitlab::Git::PreReceiveError, Gitlab::Git::CommandError, ArgumentError => error
+ raise CreateRefError, error.message
+ end
+
+ def squash_commit_message
+ merge_request.merge_params['squash_commit_message'].presence ||
+ merge_request.default_squash_commit_message(user: current_user)
+ end
+ strong_memoize_attr :squash_commit_message
+
+ def merge_commit_message
+ return @merge_commit_message if @merge_commit_message.present?
+
+ @merge_commit_message = (
+ merge_request.merge_params['commit_message'].presence ||
+ merge_request.default_merge_commit_message(user: current_user)
+ )
+ end
+ end
+end
diff --git a/config/feature_flags/development/merge_trains_create_ref_service.yml b/config/feature_flags/development/merge_trains_create_ref_service.yml
new file mode 100644
index 00000000000..cd649589a93
--- /dev/null
+++ b/config/feature_flags/development/merge_trains_create_ref_service.yml
@@ -0,0 +1,8 @@
+---
+name: merge_trains_create_ref_service
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/127531
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/420161
+milestone: '16.3'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 1e9c244ee98..10da221b6d9 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -143,6 +143,8 @@
- 1
- - compliance_management_merge_requests_compliance_violations
- 1
+- - compliance_management_standards_gitlab_at_least_two_approvals
+ - 1
- - compliance_management_standards_gitlab_base
- 1
- - compliance_management_standards_gitlab_group_base
diff --git a/db/migrate/20230804133028_add_index_custom_email_verifications_on_triggered_at_and_state_started.rb b/db/migrate/20230804133028_add_index_custom_email_verifications_on_triggered_at_and_state_started.rb
new file mode 100644
index 00000000000..cf90e88f8cd
--- /dev/null
+++ b/db/migrate/20230804133028_add_index_custom_email_verifications_on_triggered_at_and_state_started.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexCustomEmailVerificationsOnTriggeredAtAndStateStarted < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'i_custom_email_verifications_on_triggered_at_and_state_started'
+
+ def up
+ add_concurrent_index :service_desk_custom_email_verifications, :triggered_at,
+ where: 'state = 0',
+ name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :service_desk_custom_email_verifications, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230804133028 b/db/schema_migrations/20230804133028
new file mode 100644
index 00000000000..5f8148ae308
--- /dev/null
+++ b/db/schema_migrations/20230804133028
@@ -0,0 +1 @@
+e63bf851a7a66f8aa0823e5c8f41eba3829494081dda54e7e39b265b0676d4da \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 73a17e81373..b07c2dda78b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -30201,6 +30201,8 @@ CREATE INDEX i_compliance_violations_on_project_id_severity_and_id ON merge_requ
CREATE INDEX i_compliance_violations_on_project_id_title_and_id ON merge_requests_compliance_violations USING btree (target_project_id, title, id);
+CREATE INDEX i_custom_email_verifications_on_triggered_at_and_state_started ON service_desk_custom_email_verifications USING btree (triggered_at) WHERE (state = 0);
+
CREATE INDEX i_dast_pre_scan_verification_steps_on_pre_scan_verification_id ON dast_pre_scan_verification_steps USING btree (dast_pre_scan_verification_id);
CREATE INDEX i_dast_profiles_tags_on_scanner_profiles_id ON dast_profiles_tags USING btree (dast_profile_id);
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index 17720dc97ea..dc8f000d4c4 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -33,24 +33,23 @@ System: Ubuntu 20.04
Proxy: no
Current User: git
Using RVM: no
-Ruby Version: 2.6.6p146
-Gem Version: 2.7.10
-Bundler Version:1.17.3
-Rake Version: 12.3.3
-Redis Version: 5.0.9
-Git Version: 2.27.0
-Sidekiq Version:5.2.9
+Ruby Version: 2.7.6p219
+Gem Version: 3.1.6
+Bundler Version:2.3.15
+Rake Version: 13.0.6
+Redis Version: 6.2.7
+Sidekiq Version:6.4.2
Go Version: unknown
GitLab information
-Version: 13.2.2-ee
-Revision: 618883a1f9d
+Version: 15.5.5-ee
+Revision: 5f5109f142d
Directory: /opt/gitlab/embedded/service/gitlab-rails
DB Adapter: PostgreSQL
-DB Version: 11.7
-URL: http://gitlab.example.com
-HTTP Clone URL: http://gitlab.example.com/some-group/some-project.git
-SSH Clone URL: git@gitlab.example.com:some-group/some-project.git
+DB Version: 13.8
+URL: https://app.gitaly.gcp.gitlabsandbox.net
+HTTP Clone URL: https://app.gitaly.gcp.gitlabsandbox.net/some-group/some-project.git
+SSH Clone URL: git@app.gitaly.gcp.gitlabsandbox.net:some-group/some-project.git
Elasticsearch: no
Geo: no
Using LDAP: no
@@ -58,10 +57,20 @@ Using Omniauth: yes
Omniauth Providers:
GitLab Shell
-Version: 13.3.0
+Version: 14.12.0
Repository storage paths:
-- default: /var/opt/gitlab/git-data/repositories
-GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell
+- default: /var/opt/gitlab/git-data/repositories
+- gitaly: /var/opt/gitlab/git-data/repositories
+GitLab Shell path: /opt/gitlab/embedded/service/gitlab-shell
+
+
+Gitaly
+- default Address: unix:/var/opt/gitlab/gitaly/gitaly.socket
+- default Version: 15.5.5
+- default Git Version: 2.37.1.gl1
+- gitaly Address: tcp://10.128.20.6:2305
+- gitaly Version: 15.5.5
+- gitaly Git Version: 2.37.1.gl1
```
## Show GitLab license information **(PREMIUM SELF)**
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 57f97a9f54c..94607a80763 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -25336,7 +25336,10 @@ Represents a progress widget.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="workitemwidgetprogresscurrentvalue"></a>`currentValue` | [`Int`](#int) | Current value of the work item. |
+| <a id="workitemwidgetprogressendvalue"></a>`endValue` | [`Int`](#int) | End value of the work item. |
| <a id="workitemwidgetprogressprogress"></a>`progress` | [`Int`](#int) | Progress of the work item. |
+| <a id="workitemwidgetprogressstartvalue"></a>`startValue` | [`Int`](#int) | Start value of the work item. |
| <a id="workitemwidgetprogresstype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
| <a id="workitemwidgetprogressupdatedat"></a>`updatedAt` | [`Time`](#time) | Timestamp of last progress update. |
@@ -25906,6 +25909,7 @@ Name of the check for the compliance standard.
| Value | Description |
| ----- | ----------- |
+| <a id="compliancestandardsadherencechecknameat_least_two_approvals"></a>`AT_LEAST_TWO_APPROVALS` | At least two approvals. |
| <a id="compliancestandardsadherencechecknameprevent_approval_by_merge_request_author"></a>`PREVENT_APPROVAL_BY_MERGE_REQUEST_AUTHOR` | Prevent approval by merge request author. |
| <a id="compliancestandardsadherencechecknameprevent_approval_by_merge_request_committers"></a>`PREVENT_APPROVAL_BY_MERGE_REQUEST_COMMITTERS` | Prevent approval by merge request committers. |
@@ -30308,6 +30312,8 @@ A time-frame defined as a closed inclusive range of two dates.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgetprogressinputcurrentvalue"></a>`currentValue` | [`Int!`](#int) | Current progress value of the work item. |
+| <a id="workitemwidgetprogressinputendvalue"></a>`endValue` | [`Int`](#int) | End value of the work item. |
+| <a id="workitemwidgetprogressinputstartvalue"></a>`startValue` | [`Int`](#int) | Start value of the work item. |
### `WorkItemWidgetStartAndDueDateUpdateInput`
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 790d38fdd49..18efe1e6a8e 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -294,6 +294,7 @@ Example responses: **(PREMIUM SELF)**
> - Fields `housekeeping_full_repack_period`, `housekeeping_gc_period`, and `housekeeping_incremental_repack_period` [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106963) in GitLab 15.8. Use `housekeeping_optimize_repository_period` instead.
> - Parameters `sign_in_text` and `help_text` were [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124461) in GitLab 16.2. Use `description` parameter in the [Appearance API](../api/appearance.md) instead.
+> - Parameter `allow_account_deletion` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/412411) in GitLab 16.1.
In general, all settings are optional. Certain settings though, if enabled,
require other settings to be set to function properly. These requirements are
@@ -311,9 +312,10 @@ listed in the descriptions of the relevant settings.
| `after_sign_up_text` | string | no | Text shown to the user after signing up. |
| `akismet_api_key` | string | required by: `akismet_enabled` | API key for Akismet spam protection. |
| `akismet_enabled` | boolean | no | (**If enabled, requires:** `akismet_api_key`) Enable or disable Akismet spam protection. |
-| `allow_group_owners_to_manage_ldap` **(PREMIUM)** | boolean | no | Set to `true` to allow group owners to manage LDAP. |
-| `allow_local_requests_from_hooks_and_services` | boolean | no | (Deprecated: Use `allow_local_requests_from_web_hooks_and_services` instead) Allow requests to the local network from webhooks and integrations. |
-| `allow_local_requests_from_system_hooks` | boolean | no | Allow requests to the local network from system hooks. |
+| `allow_account_deletion` **(PREMIUM)** | boolean | no | Set to `true` to allow users to delete their accounts. |
+| `allow_group_owners_to_manage_ldap` **(PREMIUM)** | boolean | no | Set to `true` to allow group owners to manage LDAP. |
+| `allow_local_requests_from_hooks_and_services` | boolean | no | (Deprecated: Use `allow_local_requests_from_web_hooks_and_services` instead) Allow requests to the local network from webhooks and integrations. |
+| `allow_local_requests_from_system_hooks` | boolean | no | Allow requests to the local network from system hooks. |
| `allow_local_requests_from_web_hooks_and_services` | boolean | no | Allow requests to the local network from webhooks and integrations. |
| `allow_runner_registration_token` | boolean | no | Allow using a registration token to create a runner. Defaults to `true`. |
| `archive_builds_in_human_readable` | string | no | Set the duration for which the jobs are considered as old and expired. After that time passes, the jobs are archived and no longer able to be retried. Make it empty to never expire jobs. It has to be no less than 1 day, for example: <code>15 days</code>, <code>1 month</code>, <code>2 years</code>. |
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index a4b7db9361b..727c8e10d9c 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -102,7 +102,7 @@ The following table lists project permissions available for each role:
| [Issues](project/issues/index.md):<br>Create [confidential issues](project/issues/confidential_issues.md) | ✓ | ✓ | ✓ | ✓ | ✓ |
| [Issues](project/issues/index.md):<br>View [Design Management](project/issues/design_management.md) pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| [Issues](project/issues/index.md):<br>View [related issues](project/issues/related_issues.md) | ✓ | ✓ | ✓ | ✓ | ✓ |
-| [Issues](project/issues/index.md):<br>Set [weight](project/issues/issue_weight.md) | ✓ (15) | ✓ | ✓ | ✓ | ✓ |
+| [Issues](project/issues/index.md):<br>Set [weight](project/issues/issue_weight.md) | | ✓ | ✓ | ✓ | ✓ |
| [Issues](project/issues/index.md):<br>Set metadata such as labels, milestones, or assignees when creating an issue | ✓ (15) | ✓ | ✓ | ✓ | ✓ |
| [Issues](project/issues/index.md):<br>Edit metadata such labels, milestones, or assignees for an existing issue | (15) | ✓ | ✓ | ✓ | ✓ |
| [Issues](project/issues/index.md):<br>Set [parent epic](group/epics/manage_epics.md#add-an-existing-issue-to-an-epic) | | ✓ | ✓ | ✓ | ✓ |
diff --git a/doc/user/workspace/configuration.md b/doc/user/workspace/configuration.md
index b94170644d6..eea31e5c39e 100644
--- a/doc/user/workspace/configuration.md
+++ b/doc/user/workspace/configuration.md
@@ -10,21 +10,31 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/391543) in GitLab 16.0.
FLAG:
-On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../administration/feature_flags.md) named `remote_development_feature_flag`. On GitLab.com, this feature is available. The feature is not ready for production use.
+On self-managed GitLab, by default this feature is available.
+To hide the feature, an administrator can [disable the feature flag](../../administration/feature_flags.md) named `remote_development_feature_flag`.
+On GitLab.com, this feature is available.
+The feature is not ready for production use.
WARNING:
-This feature is in [Beta](../../policy/experiment-beta-support.md#beta) and subject to change without notice. To leave feedback, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/410031).
+This feature is in [Beta](../../policy/experiment-beta-support.md#beta) and subject to change without notice.
+To leave feedback, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/410031).
-You can use [workspaces](index.md) to create and manage isolated development environments for your GitLab projects. Each workspace includes its own set of dependencies, libraries, and tools, which you can customize to meet the specific needs of each project.
+You can use [workspaces](index.md) to create and manage isolated development environments for your GitLab projects.
+Each workspace includes its own set of dependencies, libraries, and tools,
+which you can customize to meet the specific needs of each project.
## Set up a workspace
### Prerequisites
-- Set up a Kubernetes cluster that the GitLab agent for Kubernetes supports. See the [supported Kubernetes versions](../clusters/agent/index.md#supported-kubernetes-versions-for-gitlab-features).
+- Set up a Kubernetes cluster that the GitLab agent for Kubernetes supports.
+ See the [supported Kubernetes versions](../clusters/agent/index.md#supported-kubernetes-versions-for-gitlab-features).
- Ensure autoscaling for the Kubernetes cluster is enabled.
-- In the Kubernetes cluster, verify that a [default storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/) is defined so that volumes can be dynamically provisioned for each workspace.
-- In the Kubernetes cluster, install an Ingress controller of your choice (for example, `ingress-nginx`) and make that controller accessible over a domain. For example, point `*.workspaces.example.dev` and `workspaces.example.dev` to the load balancer exposed by the Ingress controller.
+- In the Kubernetes cluster, verify that a [default storage class](https://kubernetes.io/docs/concepts/storage/storage-classes/)
+ is defined so that volumes can be dynamically provisioned for each workspace.
+- In the Kubernetes cluster, install an Ingress controller of your choice (for example, `ingress-nginx`)
+ and make that controller accessible over a domain. For example, point `*.workspaces.example.dev` and
+ `workspaces.example.dev` to the load balancer exposed by the Ingress controller.
- In the Kubernetes cluster, [install `gitlab-workspaces-proxy`](https://gitlab.com/gitlab-org/remote-development/gitlab-workspaces-proxy#installation-instructions).
- In the Kubernetes cluster, [install the GitLab agent for Kubernetes](../clusters/agent/install/index.md).
- Configure remote development settings for the GitLab agent with this snippet and update `dns_zone` as needed:
@@ -35,11 +45,13 @@ You can use [workspaces](index.md) to create and manage isolated development env
dns_zone: "workspaces.example.dev"
```
- You can use any agent defined under the root group of your project, provided that remote development is properly configured for that agent.
+ You can use any agent defined under the root group of your project,
+ provided that remote development is properly configured for that agent.
- You must have at least the Developer role in the root group.
- In each public project you want to use this feature for, create a [devfile](index.md#devfile):
- 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project
- 1. In the root directory of your project, create a file named `.devfile.yaml`. You can use one of the [example configurations](index.md#example-configurations).
+ 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
+ 1. In the root directory of your project, create a file named `.devfile.yaml`.
+ You can use one of the [example configurations](index.md#example-configurations).
- Ensure the container images used in the devfile support [arbitrary user IDs](index.md#arbitrary-user-ids).
### Create a workspace
@@ -50,12 +62,15 @@ To create a workspace:
1. Select **Your work**.
1. Select **Workspaces**.
1. Select **New workspace**.
-1. From the **Select project** dropdown list, [select a project with a `.devfile.yaml` file](#prerequisites). You can only create workspaces for public projects.
+1. From the **Select project** dropdown list, [select a project with a `.devfile.yaml` file](#prerequisites).
+ You can only create workspaces for public projects.
1. From the **Select cluster agent** dropdown list, select a cluster agent owned by the group the project belongs to.
-1. In **Time before automatic termination**, enter the number of hours until the workspace automatically terminates. This timeout is a safety measure to prevent a workspace from consuming excessive resources or running indefinitely.
+1. In **Time before automatic termination**, enter the number of hours until the workspace automatically terminates.
+ This timeout is a safety measure to prevent a workspace from consuming excessive resources or running indefinitely.
1. Select **Create workspace**.
-The workspace might take a few minutes to start. To open the workspace, under **Preview**, select the workspace.
+The workspace might take a few minutes to start.
+To open the workspace, under **Preview**, select the workspace.
You also have access to the terminal and can install any necessary dependencies.
## Connect to a workspace with SSH
@@ -75,7 +90,8 @@ To connect to a workspace with an SSH client:
1. For the password, enter your personal access token with at least the `read_api` scope.
-When you connect to `gitlab-workspaces-proxy` through the TCP load balancer, `gitlab-workspaces-proxy` examines the username (workspace name) and interacts with GitLab to verify:
+When you connect to `gitlab-workspaces-proxy` through the TCP load balancer,
+`gitlab-workspaces-proxy` examines the username (workspace name) and interacts with GitLab to verify:
- The personal access token
- User access to the workspace
@@ -86,7 +102,8 @@ Prerequisite:
- You must have an SSH host key for client verification.
-SSH is now enabled by default in [`gitlab-workspaces-proxy`](https://gitlab.com/gitlab-org/remote-development/gitlab-workspaces-proxy). To set up `gitlab-workspaces-proxy` with the GitLab Helm chart:
+SSH is now enabled by default in [`gitlab-workspaces-proxy`](https://gitlab.com/gitlab-org/remote-development/gitlab-workspaces-proxy).
+To set up `gitlab-workspaces-proxy` with the GitLab Helm chart:
1. Run this command:
@@ -159,7 +176,8 @@ USER gitlab-workspaces
## Disable remote development in the GitLab agent for Kubernetes
-You can stop the `remote_development` module of the GitLab agent for Kubernetes from communicating with GitLab. To disable remote development in the GitLab agent configuration, set this property:
+You can stop the `remote_development` module of the GitLab agent for Kubernetes from communicating with GitLab.
+To disable remote development in the GitLab agent configuration, set this property:
```yaml
remote_development:
@@ -167,3 +185,23 @@ remote_development:
```
If you already have running workspaces, an administrator must manually delete these workspaces in Kubernetes.
+
+## Related topics
+
+- [Quickstart guide for GitLab remote development workspaces](https://go.gitlab.com/AVKFvy)
+- [Set up your infrastructure for on-demand, cloud-based development environments in GitLab](https://go.gitlab.com/dp75xo)
+
+## Troubleshooting
+
+### `Failed to renew lease` when creating a workspace
+
+You might not be able to create a workspace due to a known issue in the GitLab agent for Kubernetes.
+The following error message might appear in the agent's log:
+
+```plaintext
+{"level":"info","time":"2023-01-01T00:00:00.000Z","msg":"failed to renew lease gitlab-agent-remote-dev-dev/agent-123XX-lock: timed out waiting for the condition\n","agent_id":XXXX}
+```
+
+This issue occurs when an agent instance cannot renew its leadership lease, which results
+in the shutdown of leader-only modules including the `remote_development` module.
+To resolve this issue, restart the agent instance.
diff --git a/doc/user/workspace/index.md b/doc/user/workspace/index.md
index e3dda1ab389..2ec023f2bbd 100644
--- a/doc/user/workspace/index.md
+++ b/doc/user/workspace/index.md
@@ -10,21 +10,30 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/391543) in GitLab 16.0.
FLAG:
-On self-managed GitLab, by default this feature is available. To hide the feature, an administrator can [disable the feature flag](../../administration/feature_flags.md) named `remote_development_feature_flag`. On GitLab.com, this feature is available. The feature is not ready for production use.
+On self-managed GitLab, by default this feature is available.
+To hide the feature, an administrator can [disable the feature flag](../../administration/feature_flags.md) named `remote_development_feature_flag`.
+On GitLab.com, this feature is available.
+The feature is not ready for production use.
WARNING:
-This feature is in [Beta](../../policy/experiment-beta-support.md#beta) and subject to change without notice. To leave feedback, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/410031).
+This feature is in [Beta](../../policy/experiment-beta-support.md#beta) and subject to change without notice.
+To leave feedback, see the [feedback issue](https://gitlab.com/gitlab-org/gitlab/-/issues/410031).
-A workspace is a virtual sandbox environment for your code in GitLab. You can use workspaces to create and manage isolated development environments for your GitLab projects. These environments ensure that different projects don't interfere with each other.
+A workspace is a virtual sandbox environment for your code in GitLab.
+You can use workspaces to create and manage isolated development environments for your GitLab projects.
+These environments ensure that different projects don't interfere with each other.
-Each workspace includes its own set of dependencies, libraries, and tools, which you can customize to meet the specific needs of each project. Workspaces use the AMD64 architecture.
+Each workspace includes its own set of dependencies, libraries, and tools,
+which you can customize to meet the specific needs of each project.
+Workspaces use the AMD64 architecture.
## Workspaces and projects
-Workspaces are scoped to a project. When you create a workspace, you must:
+Workspaces are scoped to a project.
+When you [create a workspace](configuration.md#create-a-workspace), you must:
- Assign the workspace to a specific project.
-- Select a project with a `.devfile.yaml` file.
+- Select a project with a [`.devfile.yaml`](#devfile) file.
The workspace can then interact with the GitLab API based on the permissions granted to the current user.
@@ -56,11 +65,15 @@ To clean up orphaned resources, an administrator must manually delete the worksp
## Devfile
-A devfile is a file that defines a development environment by specifying the necessary tools, languages, runtimes, and other components for a GitLab project.
+A devfile is a file that defines a development environment by specifying the necessary
+tools, languages, runtimes, and other components for a GitLab project.
-Workspaces have built-in support for devfiles. You can specify a devfile for your project in the GitLab configuration file. The devfile is used to automatically configure the development environment with the defined specifications.
+Workspaces have built-in support for devfiles.
+You can specify a devfile for your project in the GitLab configuration file.
+The devfile is used to automatically configure the development environment with the defined specifications.
-This way, you can create consistent and reproducible development environments regardless of the machine or platform you use.
+This way, you can create consistent and reproducible development environments
+regardless of the machine or platform you use.
### Relevant schema properties
@@ -104,61 +117,63 @@ components:
For more information, see the [devfile documentation](https://devfile.io/docs/2.2.0/devfile-schema).
For other examples, see the [`examples` projects](https://gitlab.com/gitlab-org/remote-development/examples).
-This container image is for demonstration purposes only. To use your own container image, see [Arbitrary user IDs](#arbitrary-user-ids).
+This container image is for demonstration purposes only.
+To use your own container image, see [Arbitrary user IDs](#arbitrary-user-ids).
## Web IDE
-Workspaces are bundled with the Web IDE by default. The Web IDE is the only code editor available for workspaces.
+Workspaces are bundled with the Web IDE by default.
+The Web IDE is the only code editor available for workspaces.
-The Web IDE is powered by the [GitLab VS Code fork](https://gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork). For more information, see [Web IDE](../project/web_ide/index.md).
+The Web IDE is powered by the [GitLab VS Code fork](https://gitlab.com/gitlab-org/gitlab-web-ide-vscode-fork).
+For more information, see [Web IDE](../project/web_ide/index.md).
## Private repositories
-You cannot create a workspace for a private repository because GitLab does not inject any credentials into the workspace. You can only create a workspace for public repositories that have a devfile.
+You cannot [create a workspace](configuration.md#create-a-workspace) for a private repository
+because GitLab does not inject any credentials into the workspace.
+You can only create a workspace for public repositories that have a devfile.
From a workspace, you can clone any repository manually.
## Pod interaction in a cluster
-Workspaces run as pods in a Kubernetes cluster. GitLab does not impose any restrictions on the manner in which pods interact with each other.
+Workspaces run as pods in a Kubernetes cluster.
+GitLab does not impose any restrictions on the manner in which pods interact with each other.
Because of this requirement, you might want to isolate this feature from other containers in your cluster.
## Network access and workspace authorization
-It's the client's responsibility to restrict network access to the Kubernetes control plane as GitLab does not have control over the API.
+It's the client's responsibility to restrict network access to the Kubernetes control plane
+because GitLab does not have control over the API.
-Only the workspace creator can access the workspace and any endpoints exposed in that workspace. The workspace creator is only authorized to access the workspace after user authentication with OAuth.
+Only the workspace creator can access the workspace and any endpoints exposed in that workspace.
+The workspace creator is only authorized to access the workspace after user authentication with OAuth.
## Compute resources and volume storage
-When you stop a workspace, the compute resources for that workspace are scaled down to zero. However, the volume provisioned for the workspace still exists.
+When you stop a workspace, the compute resources for that workspace are scaled down to zero.
+However, the volume provisioned for the workspace still exists.
To delete the provisioned volume, you must terminate the workspace.
## Arbitrary user IDs
-You can provide your own container image, which can run as any Linux user ID. It's not possible for GitLab to predict the Linux user ID for a container image.
-GitLab uses the Linux root group ID permission to create, update, or delete files in a container. The container runtime used by the Kubernetes cluster must ensure all containers have a default Linux group ID of `0`.
+You can provide your own container image, which can run as any Linux user ID.
-If you have a container image that does not support arbitrary user IDs, you cannot create, update, or delete files in a workspace. To create a container image that supports arbitrary user IDs, see [Create a custom workspace image that supports arbitrary user IDs](../workspace/create_image.md).
+It's not possible for GitLab to predict the Linux user ID for a container image.
+GitLab uses the Linux root group ID permission to create, update, or delete files in a container.
+The container runtime used by the Kubernetes cluster must ensure all containers have a default Linux group ID of `0`.
-For more information, see the [OpenShift documentation](https://docs.openshift.com/container-platform/4.12/openshift_images/create-images.html#use-uid_create-images).
+If you have a container image that does not support arbitrary user IDs,
+you cannot create, update, or delete files in a workspace.
+To create a container image that supports arbitrary user IDs,
+see [Create a custom workspace image that supports arbitrary user IDs](../workspace/create_image.md).
+
+For more information, see the
+[OpenShift documentation](https://docs.openshift.com/container-platform/4.12/openshift_images/create-images.html#use-uid_create-images).
## Related topics
-- [Quickstart guide for GitLab remote development workspaces](https://go.gitlab.com/AVKFvy)
-- [Set up your infrastructure for on-demand, cloud-based development environments in GitLab](https://go.gitlab.com/dp75xo)
- [GitLab workspaces demo](https://go.gitlab.com/qtu66q)
-
-## Troubleshooting
-
-### `Failed to renew lease` when creating a workspace
-
-You might not be able to create a workspace due to a known issue in the GitLab agent for Kubernetes. The following error message might appear in the agent's log:
-
-```plaintext
-{"level":"info","time":"2023-01-01T00:00:00.000Z","msg":"failed to renew lease gitlab-agent-remote-dev-dev/agent-123XX-lock: timed out waiting for the condition\n","agent_id":XXXX}
-```
-
-This issue occurs when an agent instance cannot renew its leadership lease, which results in the shutdown of leader-only modules including the `remote_development` module. To resolve this issue, restart the agent instance.
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 1bb92b7fa62..cafa75d5f59 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -222,11 +222,11 @@ module Gitlab
return unless valid_scoped_token?(token, all_available_scopes)
- if project && token.user.project_bot?
+ if project && (token.user.project_bot? || token.user.service_account?)
return unless can_read_project?(token.user, project)
end
- if token.user.can_log_in_with_non_expired_password? || token.user.project_bot?
+ if token.user.can_log_in_with_non_expired_password? || (token.user.project_bot? || token.user.service_account?)
::PersonalAccessTokens::LastUsedService.new(token).execute
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
@@ -238,7 +238,7 @@ module Gitlab
end
def bot_user_can_read_project?(user, project)
- (user.project_bot? || user.security_policy_bot?) && can_read_project?(user, project)
+ (user.project_bot? || user.service_account? || user.security_policy_bot?) && can_read_project?(user, project)
end
def valid_oauth_token?(token)
diff --git a/lib/gitlab/blame.rb b/lib/gitlab/blame.rb
index e210c18e3d1..ac39daf375f 100644
--- a/lib/gitlab/blame.rb
+++ b/lib/gitlab/blame.rb
@@ -20,13 +20,13 @@ module Gitlab
current_group = nil
i = first_line - 1
- blame.each do |commit, line, previous_path|
+ blame.each do |commit, line, previous_path, span|
commit = Commit.new(commit, project)
commit.lazy_author # preload author
if prev_sha != commit.sha
groups << current_group if current_group
- current_group = { commit: commit, lines: [], previous_path: previous_path }
+ current_group = { commit: commit, lines: [], previous_path: previous_path, span: span, lineno: i + 1 }
end
current_group[:lines] << (highlight ? highlighted_lines[i].html_safe : line)
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 30977adaea1..21d2eaec041 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -18,7 +18,7 @@ module Gitlab
def each
@blames.each do |blame|
- yield(blame.commit, blame.line, blame.previous_path)
+ yield(blame.commit, blame.line, blame.previous_path, blame.span)
end
end
@@ -49,12 +49,12 @@ module Gitlab
output.split("\n").each do |line|
if line[0, 1] == "\t"
lines << line[1, line.size]
- elsif m = /^(\w{40}) (\d+) (\d+)/.match(line)
+ elsif m = /^(\w{40}) (\d+) (\d+)\s?(\d+)?/.match(line)
# Removed these instantiations for performance but keeping them for reference:
- # commit_id, old_lineno, lineno = m[1], m[2].to_i, m[3].to_i
+ # commit_id, old_lineno, lineno, span = m[1], m[2].to_i, m[3].to_i, m[4].to_i
commit_id = m[1]
commits[commit_id] = nil unless commits.key?(commit_id)
- info[m[3].to_i] = [commit_id, m[2].to_i]
+ info[m[3].to_i] = [commit_id, m[2].to_i, m[4].to_i]
# Assumption: the first line returned by git blame is lowest-numbered
# This is true unless we start passing it `--incremental`.
@@ -72,13 +72,14 @@ module Gitlab
end
# get it together
- info.sort.each do |lineno, (commit_id, old_lineno)|
+ info.sort.each do |lineno, (commit_id, old_lineno, span)|
final << BlameLine.new(
lineno,
old_lineno,
commits[commit_id],
lines[lineno - start_line],
- previous_paths[commit_id]
+ previous_paths[commit_id],
+ span
)
end
@@ -87,14 +88,15 @@ module Gitlab
end
class BlameLine
- attr_accessor :lineno, :oldlineno, :commit, :line, :previous_path
+ attr_accessor :lineno, :oldlineno, :commit, :line, :previous_path, :span
- def initialize(lineno, oldlineno, commit, line, previous_path)
+ def initialize(lineno, oldlineno, commit, line, previous_path, span)
@lineno = lineno
@oldlineno = oldlineno
@commit = commit
@line = line
@previous_path = previous_path
+ @span = span
end
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index e4176d1eab1..11edf4f30fe 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -978,6 +978,18 @@ module Gitlab
end
end
+ def rebase_to_ref(user, source_sha:, target_ref:, first_parent_ref:, expected_old_oid: "")
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_rebase_to_ref(
+ user,
+ source_sha: source_sha,
+ target_ref: target_ref,
+ first_parent_ref: first_parent_ref,
+ expected_old_oid: expected_old_oid
+ )
+ end
+ end
+
def squash(user, start_sha:, end_sha:, author:, message:)
wrapped_gitaly_errors do
gitaly_operation_client.user_squash(user, start_sha, end_sha, author, message)
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 67e135bb530..fe76543548b 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -135,7 +135,7 @@ module Gitlab
end
end
- def user_merge_to_ref(user, source_sha:, branch:, target_ref:, message:, first_parent_ref:)
+ def user_merge_to_ref(user, source_sha:, branch:, target_ref:, message:, first_parent_ref:, expected_old_oid: "")
request = Gitaly::UserMergeToRefRequest.new(
repository: @gitaly_repo,
source_sha: source_sha,
@@ -144,6 +144,7 @@ module Gitlab
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
message: encode_binary(message),
first_parent_ref: encode_binary(first_parent_ref),
+ expected_old_oid: expected_old_oid,
timestamp: Google::Protobuf::Timestamp.new(seconds: Time.now.utc.to_i)
)
@@ -344,6 +345,23 @@ module Gitlab
request_enum.close
end
+ def user_rebase_to_ref(user, source_sha:, target_ref:, first_parent_ref:, expected_old_oid: "")
+ request = Gitaly::UserRebaseToRefRequest.new(
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ repository: @gitaly_repo,
+ source_sha: source_sha,
+ target_ref: encode_binary(target_ref),
+ first_parent_ref: encode_binary(first_parent_ref),
+ expected_old_oid: expected_old_oid,
+ timestamp: Google::Protobuf::Timestamp.new(seconds: Time.now.utc.to_i)
+ )
+
+ response = gitaly_client_call(@repository.storage, :operation_service,
+ :user_rebase_to_ref, request, timeout: GitalyClient.long_timeout)
+
+ response.commit_id
+ end
+
def user_squash(user, start_sha, end_sha, author, message, time = Time.now.utc)
request = Gitaly::UserSquashRequest.new(
repository: @gitaly_repo,
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index 4f7053b7629..26ffe2c3f7b 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -90,6 +90,19 @@ namespace :gitlab do
puts "- #{name}: \t#{repository_storage.gitaly_address}"
end
puts "GitLab Shell path:\t\t#{Gitlab.config.gitlab_shell.path}"
+
+ # check Gitaly version
+ puts ""
+ puts "Gitaly".color(:yellow)
+ Gitlab.config.repositories.storages.each do |storage_name, storage|
+ gitaly_server_service = Gitlab::GitalyClient::ServerService.new(storage_name)
+ gitaly_server_info = gitaly_server_service.info
+ puts "- #{storage_name} Address: \t#{storage.gitaly_address}"
+ puts "- #{storage_name} Version: \t#{gitaly_server_info.server_version}"
+ puts "- #{storage_name} Git Version: \t#{gitaly_server_info.git_version}"
+ rescue GRPC::DeadlineExceeded
+ puts "Unable to reach storage #{storage_name}".color(red)
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7ee46f14dab..d58d6c87fd9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -15491,6 +15491,9 @@ msgstr ""
msgid "Delete corpus"
msgstr ""
+msgid "Delete custom emoji"
+msgstr ""
+
msgid "Delete deploy key"
msgstr ""
@@ -19421,6 +19424,9 @@ msgstr ""
msgid "Failed to create wiki"
msgstr ""
+msgid "Failed to delete custom emoji. Please try again."
+msgstr ""
+
msgid "Failed to deploy to"
msgstr ""
@@ -39095,9 +39101,6 @@ msgstr ""
msgid "Remove user from project"
msgstr ""
-msgid "Remove..."
-msgstr ""
-
msgid "Removed"
msgstr ""
@@ -45154,7 +45157,7 @@ msgstr ""
msgid "StatusCheck|Apply this status check to all branches or a specific protected branch."
msgstr ""
-msgid "StatusCheck|Check for a status response in merge requests. %{link_start}Learn more%{link_end}."
+msgid "StatusCheck|Check for a status response in merge requests. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "StatusCheck|Examples: QA, Security."
diff --git a/spec/frontend/custom_emoji/components/delete_item_spec.js b/spec/frontend/custom_emoji/components/delete_item_spec.js
new file mode 100644
index 00000000000..06c4ca8d54b
--- /dev/null
+++ b/spec/frontend/custom_emoji/components/delete_item_spec.js
@@ -0,0 +1,89 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import * as Sentry from '@sentry/browser';
+import { GlModal } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import DeleteItem from '~/custom_emoji/components/delete_item.vue';
+import deleteCustomEmojiMutation from '~/custom_emoji/queries/delete_custom_emoji.mutation.graphql';
+import { CUSTOM_EMOJI } from '../mock_data';
+
+jest.mock('~/alert');
+jest.mock('@sentry/browser');
+
+let wrapper;
+let deleteMutationSpy;
+
+Vue.use(VueApollo);
+
+function createSuccessSpy() {
+ deleteMutationSpy = jest.fn().mockResolvedValue({
+ data: { destroyCustomEmoji: { customEmoji: { id: CUSTOM_EMOJI[0].id } } },
+ });
+}
+
+function createErrorSpy() {
+ deleteMutationSpy = jest.fn().mockRejectedValue();
+}
+
+function createMockApolloProvider() {
+ const requestHandlers = [[deleteCustomEmojiMutation, deleteMutationSpy]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent() {
+ const apolloProvider = createMockApolloProvider();
+
+ wrapper = mountExtended(DeleteItem, {
+ apolloProvider,
+ propsData: {
+ emoji: CUSTOM_EMOJI[0],
+ },
+ });
+}
+
+const findDeleteButton = () => wrapper.findByTestId('delete-button');
+const findModal = () => wrapper.findComponent(GlModal);
+
+describe('Custom emoji delete item component', () => {
+ it('opens modal when clicking button', async () => {
+ createSuccessSpy();
+ createComponent();
+
+ await findDeleteButton().trigger('click');
+
+ expect(document.querySelector('.gl-modal')).not.toBe(null);
+ });
+
+ it('calls GraphQL mutation on modals primary action', () => {
+ createSuccessSpy();
+ createComponent();
+
+ findModal().vm.$emit('primary');
+
+ expect(deleteMutationSpy).toHaveBeenCalledWith({ id: CUSTOM_EMOJI[0].id });
+ });
+
+ it('creates alert when mutation fails', async () => {
+ createErrorSpy();
+ createComponent();
+
+ findModal().vm.$emit('primary');
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith('Failed to delete custom emoji. Please try again.');
+ });
+
+ it('calls sentry when mutation fails', async () => {
+ createErrorSpy();
+ createComponent();
+
+ findModal().vm.$emit('primary');
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/custom_emoji/components/list_spec.js b/spec/frontend/custom_emoji/components/list_spec.js
index 2bffe367435..b5729d59464 100644
--- a/spec/frontend/custom_emoji/components/list_spec.js
+++ b/spec/frontend/custom_emoji/components/list_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import List from '~/custom_emoji/components/list.vue';
+import DeleteItem from '~/custom_emoji/components/delete_item.vue';
import { CUSTOM_EMOJI } from '../mock_data';
jest.mock('~/lib/utils/datetime/date_format_utility', () => ({
@@ -58,4 +59,21 @@ describe('Custom emoji settings list component', () => {
expect(wrapper.emitted('input')[0]).toEqual([emits]);
});
});
+
+ describe('delete button', () => {
+ it.each`
+ deleteCustomEmoji | rendersText | renders
+ ${true} | ${'renders'} | ${true}
+ ${false} | ${'does not render'} | ${false}
+ `(
+ '$rendersText delete button when deleteCustomEmoji is $deleteCustomEmoji',
+ ({ deleteCustomEmoji, renders }) => {
+ createComponent({
+ customEmojis: [{ ...CUSTOM_EMOJI[0], userPermissions: { deleteCustomEmoji } }],
+ });
+
+ expect(wrapper.findComponent(DeleteItem).exists()).toBe(renders);
+ },
+ );
+ });
});
diff --git a/spec/frontend/custom_emoji/mock_data.js b/spec/frontend/custom_emoji/mock_data.js
index 9936274d71c..f2b32bf1cfb 100644
--- a/spec/frontend/custom_emoji/mock_data.js
+++ b/spec/frontend/custom_emoji/mock_data.js
@@ -4,6 +4,9 @@ export const CUSTOM_EMOJI = [
name: 'confused_husky',
url: 'https://gitlab.com/custom_emoji/custom_emoji/-/raw/main/img/confused_husky.gif',
createdAt: 'created-at',
+ userPermissions: {
+ deleteCustomEmoji: false,
+ },
},
];
diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
new file mode 100644
index 00000000000..9a20e2ec98f
--- /dev/null
+++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
@@ -0,0 +1,179 @@
+import { GlLabel, GlIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/shared/work_item_link_child_metadata.vue';
+
+import { createAlert } from '~/alert';
+import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
+
+import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue';
+import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
+import { TASK_TYPE_NAME, WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '~/work_items/constants';
+
+import {
+ workItemTask,
+ workItemObjectiveWithChild,
+ workItemObjectiveNoMetadata,
+ confidentialWorkItemTask,
+ closedWorkItemTask,
+ workItemObjectiveMetadataWidgets,
+} from '../../mock_data';
+
+jest.mock('~/alert');
+
+describe('WorkItemLinkChildContents', () => {
+ Vue.use(VueApollo);
+
+ const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
+ let wrapper;
+ const { LABELS } = workItemObjectiveMetadataWidgets;
+ const mockLabels = LABELS.labels.nodes;
+ const mockFullPath = 'gitlab-org/gitlab-test';
+
+ const findStatusIconComponent = () =>
+ wrapper.findByTestId('item-status-icon').findComponent(GlIcon);
+ const findConfidentialIconComponent = () => wrapper.findByTestId('confidential-icon');
+ const findTitleEl = () => wrapper.findByTestId('item-title');
+ const findStatusTooltipComponent = () => wrapper.findComponent(RichTimestampTooltip);
+ const findMetadataComponent = () => wrapper.findComponent(WorkItemLinkChildMetadata);
+ const findAllLabels = () => wrapper.findAllComponents(GlLabel);
+ const findRegularLabel = () => findAllLabels().at(0);
+ const findScopedLabel = () => findAllLabels().at(1);
+ const findLinksMenuComponent = () => wrapper.findComponent(WorkItemLinksMenu);
+
+ const createComponent = ({
+ canUpdate = true,
+ parentWorkItemId = WORK_ITEM_ID,
+ childItem = workItemTask,
+ workItemType = TASK_TYPE_NAME,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemLinkChildContents, {
+ propsData: {
+ canUpdate,
+ parentWorkItemId,
+ childItem,
+ workItemType,
+ fullPath: mockFullPath,
+ childPath: '/gitlab-org/gitlab-test/-/work_items/4',
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createAlert.mockClear();
+ });
+
+ it.each`
+ status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents
+ ${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'}
+ ${'closed'} | ${closedWorkItemTask} | ${'issue-close'} | ${'gl-text-blue-500'} | ${closedWorkItemTask.closedAt} | ${'Closed'}
+ `(
+ 'renders item status icon and tooltip when item status is `$status`',
+ ({ childItem, statusIconName, statusIconColorClass, rawTimestamp, tooltipContents }) => {
+ createComponent({ childItem });
+
+ expect(findStatusIconComponent().props('name')).toBe(statusIconName);
+ expect(findStatusIconComponent().classes()).toContain(statusIconColorClass);
+ expect(findStatusTooltipComponent().props('rawTimestamp')).toBe(rawTimestamp);
+ expect(findStatusTooltipComponent().props('timestampTypeText')).toContain(tooltipContents);
+ },
+ );
+
+ it('renders confidential icon when item is confidential', () => {
+ createComponent({ childItem: confidentialWorkItemTask });
+
+ expect(findConfidentialIconComponent().props('name')).toBe('eye-slash');
+ expect(findConfidentialIconComponent().attributes('title')).toBe('Confidential');
+ });
+
+ describe('item title', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders item title', () => {
+ expect(findTitleEl().attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4');
+ expect(findTitleEl().text()).toBe(workItemTask.title);
+ });
+
+ it.each`
+ action | event | emittedEvent
+ ${'on mouseover'} | ${'mouseover'} | ${'mouseover'}
+ ${'on mouseout'} | ${'mouseout'} | ${'mouseout'}
+ `('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
+ findTitleEl().vm.$emit(event);
+
+ expect(wrapper.emitted(emittedEvent)).toEqual([[]]);
+ });
+
+ it('emits click event with correct parameters on clicking title', () => {
+ const eventObj = {
+ preventDefault: jest.fn(),
+ };
+ findTitleEl().vm.$emit('click', eventObj);
+
+ expect(wrapper.emitted('click')).toEqual([[eventObj]]);
+ });
+ });
+
+ describe('item metadata', () => {
+ beforeEach(() => {
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+ });
+
+ it('renders item metadata component when item has metadata present', () => {
+ expect(findMetadataComponent().props()).toMatchObject({
+ metadataWidgets: workItemObjectiveMetadataWidgets,
+ });
+ });
+
+ it('does not render item metadata component when item has no metadata present', () => {
+ createComponent({
+ childItem: workItemObjectiveNoMetadata,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
+
+ expect(findMetadataComponent().exists()).toBe(false);
+ });
+
+ it('renders labels', () => {
+ const mockLabel = mockLabels[0];
+
+ expect(findAllLabels()).toHaveLength(mockLabels.length);
+ expect(findRegularLabel().props()).toMatchObject({
+ title: mockLabel.title,
+ backgroundColor: mockLabel.color,
+ description: mockLabel.description,
+ scoped: false,
+ });
+ expect(findScopedLabel().props('scoped')).toBe(true); // Second label is scoped
+ });
+ });
+
+ describe('item menu', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders work-item-links-menu', () => {
+ expect(findLinksMenuComponent().exists()).toBe(true);
+ });
+
+ it('does not render work-item-links-menu when canUpdate is false', () => {
+ createComponent({ canUpdate: false });
+
+ expect(findLinksMenuComponent().exists()).toBe(false);
+ });
+
+ it('removeChild event on menu triggers `click-remove-child` event', () => {
+ findLinksMenuComponent().vm.$emit('removeChild');
+
+ expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_metadata_spec.js
index 07efb1c5ac8..25ef0e69a40 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_link_child_metadata_spec.js
@@ -3,7 +3,7 @@ import { GlAvatarsInline } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ItemMilestone from '~/issuable/components/issue_milestone.vue';
-import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue';
+import WorkItemLinkChildMetadata from '~/work_items/components/shared/work_item_link_child_metadata.vue';
import { workItemObjectiveMetadataWidgets } from '../../mock_data';
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
index f02a9fbd021..721db6c3315 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_links_menu_spec.js
@@ -1,7 +1,7 @@
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
+import WorkItemLinksMenu from '~/work_items/components/shared/work_item_links_menu.vue';
describe('WorkItemLinksMenu', () => {
let wrapper;
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index 71d1a0e253f..803ff950cbe 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -1,20 +1,16 @@
-import { GlLabel, GlIcon } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
-
import { createAlert } from '~/alert';
-import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
-import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue';
+import WorkItemLinkChildContents from '~/work_items/components/shared/work_item_link_child_contents.vue';
import {
WIDGET_TYPE_HIERARCHY,
TASK_TYPE_NAME,
@@ -24,12 +20,8 @@ import {
import {
workItemTask,
workItemObjectiveWithChild,
- workItemObjectiveNoMetadata,
- confidentialWorkItemTask,
- closedWorkItemTask,
workItemHierarchyTreeResponse,
workItemHierarchyTreeFailureResponse,
- workItemObjectiveMetadataWidgets,
changeIndirectWorkItemParentMutationResponse,
workItemUpdateFailureResponse,
} from '../../mock_data';
@@ -41,8 +33,6 @@ describe('WorkItemLinkChild', () => {
let wrapper;
let getWorkItemTreeQueryHandler;
let mutationChangeParentHandler;
- const { LABELS } = workItemObjectiveMetadataWidgets;
- const mockLabels = LABELS.labels.nodes;
const $toast = {
show: jest.fn(),
@@ -51,6 +41,8 @@ describe('WorkItemLinkChild', () => {
Vue.use(VueApollo);
+ const findWorkItemLinkChildContents = () => wrapper.findComponent(WorkItemLinkChildContents);
+
const createComponent = ({
canUpdate = true,
issuableGid = WORK_ITEM_ID,
@@ -89,87 +81,7 @@ describe('WorkItemLinkChild', () => {
createAlert.mockClear();
});
- it.each`
- status | childItem | statusIconName | statusIconColorClass | rawTimestamp | tooltipContents
- ${'open'} | ${workItemTask} | ${'issue-open-m'} | ${'gl-text-green-500'} | ${workItemTask.createdAt} | ${'Created'}
- ${'closed'} | ${closedWorkItemTask} | ${'issue-close'} | ${'gl-text-blue-500'} | ${closedWorkItemTask.closedAt} | ${'Closed'}
- `(
- 'renders item status icon and tooltip when item status is `$status`',
- ({ childItem, statusIconName, statusIconColorClass, rawTimestamp, tooltipContents }) => {
- createComponent({ childItem });
-
- const statusIcon = wrapper.findByTestId('item-status-icon').findComponent(GlIcon);
- const statusTooltip = wrapper.findComponent(RichTimestampTooltip);
-
- expect(statusIcon.props('name')).toBe(statusIconName);
- expect(statusIcon.classes()).toContain(statusIconColorClass);
- expect(statusTooltip.props('rawTimestamp')).toBe(rawTimestamp);
- expect(statusTooltip.props('timestampTypeText')).toContain(tooltipContents);
- },
- );
-
- it('renders confidential icon when item is confidential', () => {
- createComponent({ childItem: confidentialWorkItemTask });
-
- const confidentialIcon = wrapper.findByTestId('confidential-icon');
-
- expect(confidentialIcon.props('name')).toBe('eye-slash');
- expect(confidentialIcon.attributes('title')).toBe('Confidential');
- });
-
- describe('item title', () => {
- let titleEl;
-
- beforeEach(() => {
- createComponent();
-
- titleEl = wrapper.findByTestId('item-title');
- });
-
- it('renders item title', () => {
- expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4');
- expect(titleEl.text()).toBe(workItemTask.title);
- });
-
- describe('renders item title correctly for relative instance', () => {
- beforeEach(() => {
- window.gon = { relative_url_root: '/test' };
- createComponent();
- titleEl = wrapper.findByTestId('item-title');
- });
-
- it('renders item title with correct href', () => {
- expect(titleEl.attributes('href')).toBe('/test/gitlab-org/gitlab-test/-/work_items/4');
- });
-
- it('renders item title with correct text', () => {
- expect(titleEl.text()).toBe(workItemTask.title);
- });
- });
-
- it.each`
- action | event | emittedEvent
- ${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
- ${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'}
- `('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
- titleEl.vm.$emit(event);
-
- expect(wrapper.emitted(emittedEvent)).toEqual([[]]);
- });
-
- it('emits click event with correct parameters on clicking title', () => {
- const eventObj = {
- preventDefault: jest.fn(),
- };
- titleEl.vm.$emit('click', eventObj);
-
- expect(wrapper.emitted('click')).toEqual([[eventObj]]);
- });
- });
-
- describe('item metadata', () => {
- const findMetadataComponent = () => wrapper.findComponent(WorkItemLinkChildMetadata);
-
+ describe('renders WorkItemLinkChildContents', () => {
beforeEach(() => {
createComponent({
childItem: workItemObjectiveWithChild,
@@ -177,67 +89,31 @@ describe('WorkItemLinkChild', () => {
});
});
- it('renders item metadata component when item has metadata present', () => {
- const metadataEl = findMetadataComponent();
- expect(metadataEl.exists()).toBe(true);
- expect(metadataEl.props()).toMatchObject({
- metadataWidgets: workItemObjectiveMetadataWidgets,
- });
- });
-
- it('does not render item metadata component when item has no metadata present', () => {
- createComponent({
- childItem: workItemObjectiveNoMetadata,
- workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ it('with default props', () => {
+ expect(findWorkItemLinkChildContents().props()).toEqual({
+ childItem: workItemObjectiveWithChild,
+ canUpdate: true,
+ parentWorkItemId: 'gid://gitlab/WorkItem/2',
+ workItemType: 'Objective',
+ childPath: '/gitlab-org/gitlab-test/-/work_items/12',
});
-
- expect(findMetadataComponent().exists()).toBe(false);
});
- it('renders labels', () => {
- const labels = wrapper.findAllComponents(GlLabel);
- const mockLabel = mockLabels[0];
-
- expect(labels).toHaveLength(mockLabels.length);
- expect(labels.at(0).props()).toMatchObject({
- title: mockLabel.title,
- backgroundColor: mockLabel.color,
- description: mockLabel.description,
- scoped: false,
+ describe('with relative instance', () => {
+ beforeEach(() => {
+ window.gon = { relative_url_root: '/test' };
+ createComponent({
+ childItem: workItemObjectiveWithChild,
+ workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ });
});
- expect(labels.at(1).props('scoped')).toBe(true); // Second label is scoped
- });
- });
-
- describe('item menu', () => {
- let itemMenuEl;
-
- beforeEach(() => {
- createComponent();
-
- itemMenuEl = wrapper.findComponent(WorkItemLinksMenu);
- });
- it('renders work-item-links-menu', () => {
- expect(itemMenuEl.exists()).toBe(true);
-
- expect(itemMenuEl.attributes()).toMatchObject({
- 'work-item-id': workItemTask.id,
- 'parent-work-item-id': WORK_ITEM_ID,
+ it('adds the relative url to child path value', () => {
+ expect(findWorkItemLinkChildContents().props('childPath')).toBe(
+ '/test/gitlab-org/gitlab-test/-/work_items/12',
+ );
});
});
-
- it('does not render work-item-links-menu when canUpdate is false', () => {
- createComponent({ canUpdate: false });
-
- expect(wrapper.findComponent(WorkItemLinksMenu).exists()).toBe(false);
- });
-
- it('removeChild event on menu triggers `click-remove-child` event', () => {
- itemMenuEl.vm.$emit('removeChild');
-
- expect(wrapper.emitted('removeChild')).toEqual([[workItemTask]]);
- });
});
describe('nested children', () => {
@@ -252,7 +128,6 @@ describe('WorkItemLinkChild', () => {
const findFirstItem = () => getChildrenNodes()[0];
beforeEach(() => {
- getWorkItemTreeQueryHandler.mockClear();
createComponent({
childItem: workItemObjectiveWithChild,
workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index 3e406f5e74e..02e28b2ba05 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe TimeHelper do
100.32 => "1 minute and 40 seconds",
120 => "2 minutes",
121 => "2 minutes and 1 second",
- 3721 => "62 minutes and 1 second",
+ 3721 => "1 hour, 2 minutes, and 1 second",
0 => "0 seconds"
}
diff --git a/spec/lib/gitlab/blame_spec.rb b/spec/lib/gitlab/blame_spec.rb
index f636ce283ae..bfe2b7d1360 100644
--- a/spec/lib/gitlab/blame_spec.rb
+++ b/spec/lib/gitlab/blame_spec.rb
@@ -33,12 +33,18 @@ RSpec.describe Gitlab::Blame do
expect(subject.count).to eq(18)
expect(subject[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e')
expect(subject[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""])
+ expect(subject[0][:span]).to eq(3)
+ expect(subject[0][:lineno]).to eq(1)
expect(subject[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e')
expect(subject[1][:lines]).to eq(["module Popen", " extend self"])
+ expect(subject[1][:span]).to eq(2)
+ expect(subject[1][:lineno]).to eq(4)
expect(subject[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e')
expect(subject[-1][:lines]).to eq([" end", "end"])
+ expect(subject[-1][:span]).to eq(2)
+ expect(subject[-1][:lineno]).to eq(36)
end
context 'with a range 1..5' do
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
index 676ea2663d2..d21ac36bf34 100644
--- a/spec/lib/gitlab/git/blame_spec.rb
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -13,13 +13,17 @@ RSpec.describe Gitlab::Git::Blame do
let(:result) do
[].tap do |data|
- blame.each do |commit, line, previous_path|
- data << { commit: commit, line: line, previous_path: previous_path }
+ blame.each do |commit, line, previous_path, span|
+ data << { commit: commit, line: line, previous_path: previous_path, span: span }
end
end
end
describe 'blaming a file' do
+ it 'has the right commit span' do
+ expect(result.first[:span]).to eq(95)
+ end
+
it 'has the right number of lines' do
expect(result.size).to eq(95)
expect(result.first[:commit]).to be_kind_of(Gitlab::Git::Commit)
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index f8b452b157a..9055b284119 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -731,6 +731,39 @@ RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source
end
end
+ describe '#user_rebase_to_ref' do
+ let(:first_parent_ref) { 'refs/heads/my-branch' }
+ let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
+ let(:target_ref) { 'refs/merge-requests/x/merge' }
+ let(:response) { Gitaly::UserRebaseToRefResponse.new(commit_id: 'new-commit-id') }
+
+ let(:payload) do
+ { source_sha: source_sha, target_ref: target_ref, first_parent_ref: first_parent_ref }
+ end
+
+ it 'sends a user_rebase_to_ref message' do
+ freeze_time do
+ expect_any_instance_of(Gitaly::OperationService::Stub).to receive(:user_rebase_to_ref) do |_, request, options|
+ expect(options).to be_kind_of(Hash)
+ expect(request.to_h).to(
+ eq(
+ payload.merge(
+ {
+ expected_old_oid: "",
+ repository: repository.gitaly_repository.to_h,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly.to_h,
+ timestamp: { nanos: 0, seconds: Time.current.to_i }
+ }
+ )
+ )
+ )
+ end.and_return(response)
+
+ client.user_rebase_to_ref(user, **payload)
+ end
+ end
+ end
+
describe '#user_squash' do
let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' }
diff --git a/spec/services/merge_requests/create_ref_service_spec.rb b/spec/services/merge_requests/create_ref_service_spec.rb
new file mode 100644
index 00000000000..1d073cd143e
--- /dev/null
+++ b/spec/services/merge_requests/create_ref_service_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe MergeRequests::CreateRefService, feature_category: :merge_trains do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '#execute' do
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:user) { project.creator }
+ let_it_be(:first_parent_ref) { project.default_branch_or_main }
+ let_it_be(:source_branch) { 'branch' }
+ let(:target_ref) { "refs/merge-requests/#{merge_request.iid}/train" }
+ let(:source_sha) { project.commit(source_branch).sha }
+ let(:squash) { false }
+
+ let(:merge_request) do
+ create(
+ :merge_request,
+ title: 'Merge request ref test',
+ author: user,
+ source_project: project,
+ target_project: project,
+ source_branch: source_branch,
+ target_branch: first_parent_ref,
+ squash: squash
+ )
+ end
+
+ subject(:result) do
+ described_class.new(
+ current_user: user,
+ merge_request: merge_request,
+ target_ref: target_ref,
+ source_sha: source_sha,
+ first_parent_ref: first_parent_ref
+ ).execute
+ end
+
+ context 'when there is a user-caused gitaly error' do
+ let(:source_sha) { '123' }
+
+ it 'returns an error response' do
+ expect(result[:status]).to eq :error
+ end
+ end
+
+ context 'with valid inputs' do
+ before_all do
+ # ensure first_parent_ref is created before source_sha
+ project.repository.create_file(
+ user,
+ 'README.md',
+ '',
+ message: 'Base parent commit 1',
+ branch_name: first_parent_ref
+ )
+ project.repository.create_branch(source_branch, first_parent_ref)
+
+ # create two commits source_branch to test squashing
+ project.repository.create_file(
+ user,
+ '.gitlab-ci.yml',
+ '',
+ message: 'Feature branch commit 1',
+ branch_name: source_branch
+ )
+
+ project.repository.create_file(
+ user,
+ '.gitignore',
+ '',
+ message: 'Feature branch commit 2',
+ branch_name: source_branch
+ )
+
+ # create an extra commit not present on source_branch
+ project.repository.create_file(
+ user,
+ 'EXTRA',
+ '',
+ message: 'Base parent commit 2',
+ branch_name: first_parent_ref
+ )
+ end
+
+ it 'writes the merged result into target_ref', :aggregate_failures do
+ expect(result[:status]).to eq :success
+ expect(project.repository.commits(target_ref, limit: 10, order: 'topo').map(&:message)).to(
+ match(
+ [
+ a_string_matching(/Merge branch '#{source_branch}' into '#{first_parent_ref}'/),
+ 'Feature branch commit 2',
+ 'Feature branch commit 1',
+ 'Base parent commit 2',
+ 'Base parent commit 1'
+ ]
+ )
+ )
+ end
+
+ context 'when squash is requested' do
+ let(:squash) { true }
+
+ it 'writes the squashed result', :aggregate_failures do
+ expect(result[:status]).to eq :success
+ expect(project.repository.commits(target_ref, limit: 10, order: 'topo').map(&:message)).to(
+ match(
+ [
+ a_string_matching(/Merge branch '#{source_branch}' into '#{first_parent_ref}'/),
+ "#{merge_request.title}\n",
+ 'Base parent commit 2',
+ 'Base parent commit 1'
+ ]
+ )
+ )
+ end
+ end
+
+ context 'when semi-linear merges are enabled' do
+ before do
+ project.merge_method = :rebase_merge
+ project.save!
+ end
+
+ it 'writes the semi-linear merged result', :aggregate_failures do
+ expect(result[:status]).to eq :success
+ expect(project.repository.commits(target_ref, limit: 10, order: 'topo').map(&:message)).to(
+ match(
+ [
+ a_string_matching(/Merge branch '#{source_branch}' into '#{first_parent_ref}'/),
+ 'Feature branch commit 2',
+ 'Feature branch commit 1',
+ 'Base parent commit 2',
+ 'Base parent commit 1'
+ ]
+ )
+ )
+ end
+ end
+
+ context 'when fast-forward merges are enabled' do
+ before do
+ project.merge_method = :ff
+ project.save!
+ end
+
+ it 'writes the rebased merged result', :aggregate_failures do
+ expect(result[:status]).to eq :success
+ expect(project.repository.commits(target_ref, limit: 10, order: 'topo').map(&:message)).to(
+ eq(
+ [
+ 'Feature branch commit 2',
+ 'Feature branch commit 1',
+ 'Base parent commit 2',
+ 'Base parent commit 1'
+ ]
+ )
+ )
+ end
+ end
+ end
+ end
+end