Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1faea1c6a0
commit
d56569ff3e
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0ec311c007a78701fa4ee2ed8d58ca686378fcf0
|
||||
38aa3f1b70406c72e9ab26389cbdecbdd3218a0a
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ query getCustomEmojis($groupPath: ID!, $after: String = "", $before: String = ""
|
|||
name
|
||||
url
|
||||
createdAt
|
||||
userPermissions {
|
||||
deleteCustomEmoji
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
mutation deleteCustomEmoji($id: CustomEmojiID!) {
|
||||
destroyCustomEmoji(input: { id: $id }) {
|
||||
customEmoji {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -95,6 +95,7 @@ export default {
|
|||
:target="`#${$options.toggleId}`"
|
||||
placement="bottom"
|
||||
container="#super-sidebar"
|
||||
noninteractive
|
||||
>
|
||||
{{ $options.i18n.createNew }}
|
||||
</gl-tooltip>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TimeHelper
|
||||
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_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
|
||||
|
||||
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
|
||||
def time_interval_in_words(interval_in_seconds)
|
||||
time_parts = ActiveSupport::Duration.build(interval_in_seconds.to_i).parts
|
||||
|
||||
time_parts.map { |unit, value| TIME_UNIT_TRANSLATION[unit].call(value) }.to_sentence
|
||||
end
|
||||
|
||||
def duration_in_numbers(duration_in_seconds)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
e63bf851a7a66f8aa0823e5c8f41eba3829494081dda54e7e39b265b0676d4da
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)**
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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>. |
|
||||
|
|
|
|||
|
|
@ -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) | | ✓ | ✓ | ✓ | ✓ |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,66 +89,30 @@ 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('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',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render item metadata component when item has no metadata present', () => {
|
||||
createComponent({
|
||||
childItem: workItemObjectiveNoMetadata,
|
||||
workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
|
||||
describe('with relative instance', () => {
|
||||
beforeEach(() => {
|
||||
window.gon = { relative_url_root: '/test' };
|
||||
createComponent({
|
||||
childItem: workItemObjectiveWithChild,
|
||||
workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
it('adds the relative url to child path value', () => {
|
||||
expect(findWorkItemLinkChildContents().props('childPath')).toBe(
|
||||
'/test/gitlab-org/gitlab-test/-/work_items/12',
|
||||
);
|
||||
});
|
||||
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('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]]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -252,7 +128,6 @@ describe('WorkItemLinkChild', () => {
|
|||
const findFirstItem = () => getChildrenNodes()[0];
|
||||
|
||||
beforeEach(() => {
|
||||
getWorkItemTreeQueryHandler.mockClear();
|
||||
createComponent({
|
||||
childItem: workItemObjectiveWithChild,
|
||||
workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue