Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-01 15:12:07 +00:00
parent a53033814d
commit 7b197a72aa
58 changed files with 995 additions and 688 deletions

View File

@ -336,7 +336,7 @@
.qa-patterns: &qa-patterns .qa-patterns: &qa-patterns
- ".dockerignore" - ".dockerignore"
- "qa/**/*" - "{,jh/}qa/**/*"
# Code patterns + .ci-patterns # Code patterns + .ci-patterns
.code-patterns: &code-patterns .code-patterns: &code-patterns
@ -414,7 +414,7 @@
- ".gitlab/ci/**/*" - ".gitlab/ci/**/*"
# QA changes # QA changes
- ".dockerignore" - ".dockerignore"
- "qa/**/*" - "{,jh/}qa/**/*"
# Mapped patterns (see tests.yml) # Mapped patterns (see tests.yml)
- "data/whats_new/*.yml" - "data/whats_new/*.yml"
@ -448,7 +448,7 @@
- "{,spec/}tooling/**/*" - "{,spec/}tooling/**/*"
# QA changes # QA changes
- ".dockerignore" - ".dockerignore"
- "qa/**/*" - "{,jh/}qa/**/*"
# Mapped patterns (see tests.yml) # Mapped patterns (see tests.yml)
- "data/whats_new/*.yml" - "data/whats_new/*.yml"
@ -483,7 +483,7 @@
- "{,spec/}tooling/**/*" - "{,spec/}tooling/**/*"
# QA changes # QA changes
- ".dockerignore" - ".dockerignore"
- "qa/**/*" - "{,jh/}qa/**/*"
# Workhorse changes # Workhorse changes
- "GITLAB_WORKHORSE_VERSION" - "GITLAB_WORKHORSE_VERSION"
- "workhorse/**/*" - "workhorse/**/*"

View File

@ -51,20 +51,11 @@ export default {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
canDestroy: {
required: true,
type: Boolean,
},
showInlineEditButton: { showInlineEditButton: {
type: Boolean, type: Boolean,
required: false, required: false,
default: true, default: true,
}, },
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
enableAutocomplete: { enableAutocomplete: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -494,14 +485,12 @@ export default {
:endpoint="endpoint" :endpoint="endpoint"
:form-state="formState" :form-state="formState"
:initial-description-text="initialDescriptionText" :initial-description-text="initialDescriptionText"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates" :issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath" :markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:project-path="projectPath" :project-path="projectPath"
:project-id="projectId" :project-id="projectId"
:project-namespace="projectNamespace" :project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
:can-attach-file="canAttachFile" :can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
:issuable-type="issuableType" :issuable-type="issuableType"

View File

@ -13,7 +13,8 @@ export default {
props: { props: {
issuePath: { issuePath: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
issueType: { issueType: {
type: String, type: String,

View File

@ -1,12 +1,10 @@
<script> <script>
import { GlButton, GlModalDirective } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { uniqueId } from 'lodash'; import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import updateMixin from '../mixins/update'; import updateMixin from '../mixins/update';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql'; import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
import DeleteIssueModal from './delete_issue_modal.vue';
const issuableTypes = { const issuableTypes = {
issue: __('Issue'), issue: __('Issue'),
@ -18,18 +16,10 @@ const trackingMixin = Tracking.mixin({ label: 'delete_issue' });
export default { export default {
components: { components: {
DeleteIssueModal,
GlButton, GlButton,
}, },
directives: {
GlModal: GlModalDirective,
},
mixins: [trackingMixin, updateMixin], mixins: [trackingMixin, updateMixin],
props: { props: {
canDestroy: {
type: Boolean,
required: true,
},
endpoint: { endpoint: {
required: true, required: true,
type: String, type: String,
@ -38,11 +28,6 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
issuableType: { issuableType: {
type: String, type: String,
required: true, required: true,
@ -53,7 +38,6 @@ export default {
deleteLoading: false, deleteLoading: false,
skipApollo: false, skipApollo: false,
issueState: {}, issueState: {},
modalId: uniqueId('delete-issuable-modal-'),
}; };
}, },
apollo: { apollo: {
@ -68,17 +52,9 @@ export default {
}, },
}, },
computed: { computed: {
deleteIssuableButtonText() {
return sprintf(__('Delete %{issuableType}'), {
issuableType: this.typeToShow.toLowerCase(),
});
},
isSubmitEnabled() { isSubmitEnabled() {
return this.formState.title.trim() !== ''; return this.formState.title.trim() !== '';
}, },
shouldShowDeleteButton() {
return this.canDestroy && this.showDeleteButton && this.typeToShow;
},
typeToShow() { typeToShow() {
const { issueState, issuableType } = this; const { issueState, issuableType } = this;
const type = issueState.issueType ?? issuableType; const type = issueState.issueType ?? issuableType;
@ -89,52 +65,26 @@ export default {
closeForm() { closeForm() {
eventHub.$emit('close.form'); eventHub.$emit('close.form');
}, },
deleteIssuable() {
this.deleteLoading = true;
eventHub.$emit('delete.issuable');
},
}, },
}; };
</script> </script>
<template> <template>
<div class="gl-mt-3 gl-mb-3 gl-display-flex gl-justify-content-space-between"> <div class="gl-mt-3 gl-mb-3 gl-display-flex">
<div> <gl-button
<gl-button :loading="formState.updateLoading"
:loading="formState.updateLoading" :disabled="formState.updateLoading || !isSubmitEnabled"
:disabled="formState.updateLoading || !isSubmitEnabled" category="primary"
category="primary" variant="confirm"
variant="confirm" class="gl-mr-3"
class="gl-mr-3" data-testid="issuable-save-button"
data-testid="issuable-save-button" type="submit"
type="submit" @click.prevent="updateIssuable"
@click.prevent="updateIssuable" >
> {{ __('Save changes') }}
{{ __('Save changes') }} </gl-button>
</gl-button> <gl-button data-testid="issuable-cancel-button" @click="closeForm">
<gl-button data-testid="issuable-cancel-button" @click="closeForm"> {{ __('Cancel') }}
{{ __('Cancel') }} </gl-button>
</gl-button>
</div>
<div v-if="shouldShowDeleteButton">
<gl-button
v-gl-modal="modalId"
:loading="deleteLoading"
:disabled="deleteLoading"
category="secondary"
variant="danger"
data-testid="issuable-delete-button"
@click="track('click_button')"
>
{{ deleteIssuableButtonText }}
</gl-button>
<delete-issue-modal
:issue-path="endpoint"
:issue-type="typeToShow"
:modal-id="modalId"
:title="deleteIssuableButtonText"
@delete="deleteIssuable"
/>
</div>
</div> </div>
</template> </template>

View File

@ -22,10 +22,6 @@ export default {
LockedWarning, LockedWarning,
}, },
props: { props: {
canDestroy: {
type: Boolean,
required: true,
},
endpoint: { endpoint: {
type: String, type: String,
required: true, required: true,
@ -63,11 +59,6 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
showDeleteButton: {
type: Boolean,
required: false,
default: true,
},
canAttachFile: { canAttachFile: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -231,12 +222,6 @@ export default {
:enable-autocomplete="enableAutocomplete" :enable-autocomplete="enableAutocomplete"
/> />
<edit-actions <edit-actions :endpoint="endpoint" :form-state="formState" :issuable-type="issuableType" />
:endpoint="endpoint"
:form-state="formState"
:can-destroy="canDestroy"
:show-delete-button="showDeleteButton"
:issuable-type="issuableType"
/>
</form> </form>
</template> </template>

View File

@ -168,7 +168,7 @@ export default {
</p> </p>
</template> </template>
<template v-else-if="!hasPipeline"> <template v-else-if="!hasPipeline">
<gl-loading-icon size="lg" /> <gl-loading-icon size="md" />
<p <p
class="gl-flex-grow-1 gl-display-flex gl-ml-3 gl-mb-0" class="gl-flex-grow-1 gl-display-flex gl-ml-3 gl-mb-0"
data-testid="monitoring-pipeline-message" data-testid="monitoring-pipeline-message"

View File

@ -1,13 +1,23 @@
<script> <script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import StatusIcon from './mr_widget_status_icon.vue'; import StatusIcon from './mr_widget_status_icon.vue';
import Actions from './action_buttons.vue'; import Actions from './action_buttons.vue';
export default { export default {
components: { components: {
GlButton,
StatusIcon, StatusIcon,
Actions, Actions,
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
props: { props: {
mr: {
type: Object,
required: true,
},
isLoading: { isLoading: {
type: Boolean, type: Boolean,
required: false, required: false,
@ -24,6 +34,10 @@ export default {
default: () => [], default: () => [],
}, },
}, },
i18n: {
expandDetailsTooltip: __('Expand merge details'),
collapseDetailsTooltip: __('Collapse merge details'),
},
}; };
</script> </script>
@ -36,18 +50,37 @@ export default {
<slot name="icon"> <slot name="icon">
<status-icon :status="status" /> <status-icon :status="status" />
</slot> </slot>
<div <div class="gl-display-flex gl-w-full">
:class="{ 'gl-display-flex': actions.length, 'gl-md-display-flex': !actions.length }"
class="media-body"
>
<slot></slot>
<div <div
:class="{ 'gl-flex-direction-column-reverse': !actions.length }" :class="{ 'gl-display-flex': actions.length, 'gl-md-display-flex': !actions.length }"
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto" class="media-body"
> >
<slot name="actions"> <slot></slot>
<actions v-if="actions.length" :tertiary-buttons="actions" /> <div
</slot> :class="{ 'gl-flex-direction-column-reverse': !actions.length }"
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
>
<slot name="actions">
<actions v-if="actions.length" :tertiary-buttons="actions" />
</slot>
</div>
</div>
<div
class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
>
<gl-button
v-gl-tooltip
:title="
mr.mergeDetailsCollapsed
? $options.i18n.expandDetailsTooltip
: $options.i18n.collapseDetailsTooltip
"
:icon="mr.mergeDetailsCollapsed ? 'chevron-lg-down' : 'chevron-lg-up'"
category="tertiary"
size="small"
class="gl-vertical-align-top"
@click="() => mr.toggleMergeDetails()"
/>
</div> </div>
</div> </div>
</template> </template>

View File

@ -6,11 +6,17 @@ export default {
components: { components: {
StateContainer, StateContainer,
}, },
props: {
mr: {
type: Object,
required: true,
},
},
}; };
</script> </script>
<template> <template>
<state-container status="failed"> <state-container :mr="mr" status="failed">
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
{{ s__('mrWidget|Merge unavailable: merge requests are read-only on archived projects.') }} {{ s__('mrWidget|Merge unavailable: merge requests are read-only on archived projects.') }}
</span> </span>

View File

@ -150,7 +150,7 @@ export default {
}; };
</script> </script>
<template> <template>
<state-container status="scheduled" :is-loading="loading" :actions="actions"> <state-container :mr="mr" status="scheduled" :is-loading="loading" :actions="actions">
<template #loading> <template #loading>
<gl-skeleton-loader :width="334" :height="30"> <gl-skeleton-loader :width="334" :height="30">
<rect x="0" y="3" width="24" height="24" rx="4" /> <rect x="0" y="3" width="24" height="24" rx="4" />

View File

@ -58,7 +58,7 @@ export default {
}; };
</script> </script>
<template> <template>
<state-container status="failed" :actions="actions"> <state-container :mr="mr" status="failed" :actions="actions">
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
<template v-if="mergeError">{{ mergeError }}</template> <template v-if="mergeError">{{ mergeError }}</template>
{{ s__('mrWidget|This merge request failed to be merged automatically') }} {{ s__('mrWidget|This merge request failed to be merged automatically') }}

View File

@ -6,10 +6,16 @@ export default {
components: { components: {
StateContainer, StateContainer,
}, },
props: {
mr: {
type: Object,
required: true,
},
},
}; };
</script> </script>
<template> <template>
<state-container status="loading"> <state-container :mr="mr" status="loading">
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
{{ s__('mrWidget|Checking if merge request can be merged…') }} {{ s__('mrWidget|Checking if merge request can be merged…') }}
</span> </span>

View File

@ -86,7 +86,7 @@ export default {
}; };
</script> </script>
<template> <template>
<state-container status="failed" :is-loading="isLoading"> <state-container :mr="mr" status="failed" :is-loading="isLoading">
<template #loading> <template #loading>
<gl-skeleton-loader :width="334" :height="30"> <gl-skeleton-loader :width="334" :height="30">
<rect x="0" y="7" width="150" height="16" rx="4" /> <rect x="0" y="7" width="150" height="16" rx="4" />

View File

@ -95,12 +95,12 @@ export default {
}; };
</script> </script>
<template> <template>
<state-container v-if="isRefreshing" status="loading"> <state-container v-if="isRefreshing" :mr="mr" status="loading">
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
{{ s__('mrWidget|Refreshing now') }} {{ s__('mrWidget|Refreshing now') }}
</span> </span>
</state-container> </state-container>
<state-container v-else status="failed" :actions="actions"> <state-container v-else :mr="mr" status="failed" :actions="actions">
<span class="gl-font-weight-bold"> <span class="gl-font-weight-bold">
<span v-if="mr.mergeError" class="has-error-message" data-testid="merge-error"> <span v-if="mr.mergeError" class="has-error-message" data-testid="merge-error">
{{ mergeError }} {{ mergeError }}

View File

@ -150,7 +150,7 @@ export default {
}; };
</script> </script>
<template> <template>
<state-container :actions="actions" status="merged"> <state-container :mr="mr" :actions="actions" status="merged">
<mr-widget-author-time <mr-widget-author-time
:action-text="s__('mrWidget|Merged by')" :action-text="s__('mrWidget|Merged by')"
:author="mr.metrics.mergedBy" :author="mr.metrics.mergedBy"

View File

@ -152,7 +152,7 @@ export default {
}; };
</script> </script>
<template> <template>
<state-container :status="status" :is-loading="isLoading"> <state-container :mr="mr" :status="status" :is-loading="isLoading">
<template #loading> <template #loading>
<gl-skeleton-loader :width="334" :height="30"> <gl-skeleton-loader :width="334" :height="30">
<rect x="0" y="3" width="24" height="24" rx="4" /> <rect x="0" y="3" width="24" height="24" rx="4" />

View File

@ -22,7 +22,7 @@ export default {
</script> </script>
<template> <template>
<state-container status="failed"> <state-container :mr="mr" status="failed">
<span <span
class="gl-font-weight-bold gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!" class="gl-font-weight-bold gl-md-mr-3 gl-flex-grow-1 gl-ml-0! gl-text-body!"
data-qa-selector="head_mismatch_content" data-qa-selector="head_mismatch_content"

View File

@ -24,7 +24,7 @@ export default {
</script> </script>
<template> <template>
<state-container status="failed"> <state-container :mr="mr" status="failed">
<span <span
class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!" class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"
> >

View File

@ -163,7 +163,7 @@ export default {
</script> </script>
<template> <template>
<state-container status="failed"> <state-container :mr="mr" status="failed">
<span class="gl-font-weight-bold gl-ml-0! gl-text-body! gl-flex-grow-1"> <span class="gl-font-weight-bold gl-ml-0! gl-text-body! gl-flex-grow-1">
{{ __("Merge blocked: merge request must be marked as ready. It's still marked as draft.") }} {{ __("Merge blocked: merge request must be marked as ready. It's still marked as draft.") }}
</span> </span>

View File

@ -230,6 +230,11 @@ export default {
shouldShowCodeQualityExtension() { shouldShowCodeQualityExtension() {
return window.gon?.features?.refactorCodeQualityExtension; return window.gon?.features?.refactorCodeQualityExtension;
}, },
shouldShowMergeDetails() {
if (this.mr.state === 'readyToMerge') return true;
return !this.mr.mergeDetailsCollapsed;
},
}, },
watch: { watch: {
'mr.machineValue': { 'mr.machineValue': {
@ -318,6 +323,12 @@ export default {
this.initPolling(); this.initPolling();
this.bindEventHubListeners(); this.bindEventHubListeners();
eventHub.$on('mr.discussion.updated', this.checkStatus); eventHub.$on('mr.discussion.updated', this.checkStatus);
window.addEventListener('resize', () => {
if (window.innerWidth >= 768) {
this.mr.toggleMergeDetails(false);
}
});
}, },
getServiceEndpoints(store) { getServiceEndpoints(store) {
return { return {
@ -621,7 +632,12 @@ export default {
<div class="mr-widget-section" data-qa-selector="mr_widget_content"> <div class="mr-widget-section" data-qa-selector="mr_widget_content">
<component :is="componentName" :mr="mr" :service="service" /> <component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge v-if="mr.commitsCount" :mr="mr" :service="service" /> <ready-to-merge
v-if="mr.commitsCount"
v-show="shouldShowMergeDetails"
:mr="mr"
:service="service"
/>
</div> </div>
</div> </div>
<mr-widget-pipeline-container <mr-widget-pipeline-container

View File

@ -28,6 +28,7 @@ export default class MergeRequestStore {
this.stateMachine = machine(STATE_MACHINE.definition); this.stateMachine = machine(STATE_MACHINE.definition);
this.machineValue = this.stateMachine.value; this.machineValue = this.stateMachine.value;
this.mergeDetailsCollapsed = window.innerWidth < 768;
this.setPaths(data); this.setPaths(data);
@ -405,4 +406,8 @@ export default class MergeRequestStore {
this.transitionStateMachine(transitionOptions); this.transitionStateMachine(transitionOptions);
} }
toggleMergeDetails(val = !this.mergeDetailsCollapsed) {
this.mergeDetailsCollapsed = val;
}
} }

View File

@ -0,0 +1,109 @@
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import { STATE_OPEN } from '../../constants';
import WorkItemLinksMenu from './work_item_links_menu.vue';
export default {
components: {
GlButton,
GlIcon,
RichTimestampTooltip,
WorkItemLinksMenu,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
projectPath: {
type: String,
required: true,
},
canUpdate: {
type: Boolean,
required: true,
},
issuableGid: {
type: String,
required: true,
},
childItem: {
type: Object,
required: true,
},
},
computed: {
isItemOpen() {
return this.childItem.state === STATE_OPEN;
},
iconClass() {
return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
},
iconName() {
return this.isItemOpen ? 'issue-open-m' : 'issue-close';
},
stateTimestamp() {
return this.isItemOpen ? this.childItem.createdAt : this.childItem.closedAt;
},
stateTimestampTypeText() {
return this.isItemOpen ? __('Created') : __('Closed');
},
childPath() {
return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(this.childItem.id)}`;
},
},
};
</script>
<template>
<div
class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
data-testid="links-child"
>
<div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1">
<span :id="`stateIcon-${childItem.id}`" class="gl-mr-3" data-testid="item-status-icon">
<gl-icon :name="iconName" :class="iconClass" :aria-label="stateTimestampTypeText" />
</span>
<rich-timestamp-tooltip
:target="`stateIcon-${childItem.id}`"
:raw-timestamp="stateTimestamp"
:timestamp-type-text="stateTimestampTypeText"
/>
<gl-icon
v-if="childItem.confidential"
v-gl-tooltip.top
name="eye-slash"
class="gl-mr-2 gl-text-orange-500"
data-testid="confidential-icon"
:aria-label="__('Confidential')"
:title="__('Confidential')"
/>
<gl-button
:href="childPath"
category="tertiary"
variant="link"
class="gl-text-truncate gl-max-w-80 gl-text-black-normal!"
@click="$emit('click', childItem.id, $event)"
@mouseover="$emit('mouseover', childItem.id, $event)"
@mouseout="$emit('mouseout', childItem.id, $event)"
>
{{ childItem.title }}
</gl-button>
</div>
<div
v-if="canUpdate"
class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center"
>
<work-item-links-menu
:work-item-id="childItem.id"
:parent-work-item-id="issuableGid"
data-testid="links-menu"
@removeChild="$emit('remove', childItem.id)"
/>
</div>
</div>
</template>

View File

@ -9,18 +9,13 @@ import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.g
import { isMetaKey } from '~/lib/utils/common_utils'; import { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility'; import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { import { WIDGET_ICONS, WORK_ITEM_STATUS_TEXT, WIDGET_TYPE_HIERARCHY } from '../../constants';
STATE_OPEN,
WIDGET_ICONS,
WORK_ITEM_STATUS_TEXT,
WIDGET_TYPE_HIERARCHY,
} from '../../constants';
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql'; import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql'; import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import workItemQuery from '../../graphql/work_item.query.graphql'; import workItemQuery from '../../graphql/work_item.query.graphql';
import WorkItemDetailModal from '../work_item_detail_modal.vue'; import WorkItemDetailModal from '../work_item_detail_modal.vue';
import WorkItemLinkChild from './work_item_link_child.vue';
import WorkItemLinksForm from './work_item_links_form.vue'; import WorkItemLinksForm from './work_item_links_form.vue';
import WorkItemLinksMenu from './work_item_links_menu.vue';
export default { export default {
components: { components: {
@ -28,8 +23,8 @@ export default {
GlIcon, GlIcon,
GlAlert, GlAlert,
GlLoadingIcon, GlLoadingIcon,
WorkItemLinkChild,
WorkItemLinksForm, WorkItemLinksForm,
WorkItemLinksMenu,
WorkItemDetailModal, WorkItemDetailModal,
}, },
directives: { directives: {
@ -124,12 +119,6 @@ export default {
}, },
}, },
methods: { methods: {
iconClass(state) {
return state === STATE_OPEN ? 'gl-text-green-500' : 'gl-text-blue-500';
},
iconName(state) {
return state === STATE_OPEN ? 'issue-open-m' : 'issue-close';
},
toggle() { toggle() {
this.isOpen = !this.isOpen; this.isOpen = !this.isOpen;
}, },
@ -171,9 +160,6 @@ export default {
replace: true, replace: true,
}); });
}, },
childPath(childItemId) {
return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(childItemId)}`;
},
toggleChildFromCache(workItem, childId, store) { toggleChildFromCache(workItem, childId, store) {
const sourceData = store.readQuery({ const sourceData = store.readQuery({
query: getWorkItemLinksQuery, query: getWorkItemLinksQuery,
@ -322,48 +308,18 @@ export default {
@cancel="hideAddForm" @cancel="hideAddForm"
@addWorkItemChild="addChild" @addWorkItemChild="addChild"
/> />
<div <work-item-link-child
v-for="child in children" v-for="child in children"
:key="child.id" :key="child.id"
class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32" :project-path="projectPath"
data-testid="links-child" :can-update="canUpdate"
> :issuable-gid="issuableGid"
<div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1"> :child-item="child"
<gl-icon @click="openChild"
:name="iconName(child.state)" @mouseover="prefetchWorkItem"
class="gl-mr-3" @mouseout="clearPrefetching"
:class="iconClass(child.state)" @remove="removeChild"
/> />
<gl-icon
v-if="child.confidential"
v-gl-tooltip.top
name="eye-slash"
class="gl-mr-2 gl-text-orange-500"
data-testid="confidential-icon"
:title="__('Confidential')"
/>
<gl-button
:href="childPath(child.id)"
category="tertiary"
variant="link"
class="gl-text-truncate gl-max-w-80 gl-text-black-normal!"
@click="openChild(child.id, $event)"
@mouseover="prefetchWorkItem(child.id)"
@mouseout="clearPrefetching"
>
{{ child.title }}
</gl-button>
</div>
<div class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center">
<work-item-links-menu
v-if="canUpdate"
:work-item-id="child.id"
:parent-work-item-id="issuableGid"
data-testid="links-menu"
@removeChild="removeChild(child.id)"
/>
</div>
</div>
<work-item-detail-modal <work-item-detail-modal
ref="modal" ref="modal"
:work-item-id="activeChildId" :work-item-id="activeChildId"

View File

@ -26,6 +26,8 @@ query workItemLinksQuery($id: WorkItemID!) {
} }
title title
state state
createdAt
closedAt
} }
} }
} }

View File

@ -62,9 +62,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
options[:allow_tree_conflicts] options[:allow_tree_conflicts]
] ]
if Feature.enabled?(:etag_merge_request_diff_batches, @merge_request.project) return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
end
if diff_options_hash[:paths].blank? if diff_options_hash[:paths].blank?
if Feature.enabled?(:remove_caching_diff_batches, @merge_request.project) if Feature.enabled?(:remove_caching_diff_batches, @merge_request.project)

View File

@ -19,7 +19,11 @@ module Ci
def builds_for_group_runner def builds_for_group_runner
return new_builds.none if runner.namespace_ids.empty? return new_builds.none if runner.namespace_ids.empty?
new_builds.where('ci_pending_builds.namespace_traversal_ids && ARRAY[?]::int[]', runner.namespace_ids) new_builds_relation = new_builds.where('ci_pending_builds.namespace_traversal_ids && ARRAY[?]::int[]', runner.namespace_ids)
return order(new_builds_relation) if ::Feature.enabled?(:order_builds_for_group_runner)
new_builds_relation
end end
def builds_matching_tag_ids(relation, ids) def builds_matching_tag_ids(relation, ids)

View File

@ -15,7 +15,7 @@
= f.text_area :outbound_local_requests_allowlist_raw, placeholder: "example.com, 192.168.1.1, xn--itlab-j1a.com", class: 'form-control gl-form-input', rows: 8 = f.text_area :outbound_local_requests_allowlist_raw, placeholder: "example.com, 192.168.1.1, xn--itlab-j1a.com", class: 'form-control gl-form-input', rows: 8
%span.form-text.text-muted %span.form-text.text-muted
= s_('OutboundRequests|Requests to these domains and IP addresses are accessible to both system hooks and web hooks even when local requests are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 and 127.0.0.0/28 are supported. Domain wildcards are not supported. To separate entries use commas, semicolons, or newlines. The allowlist can hold a maximum of 1000 entries. Domains must be IDNA encoded.') = s_('OutboundRequests|Requests to these domains and IP addresses are accessible to both system hooks and web hooks even when local requests are not allowed. IP ranges such as 1:0:0:0:0:0:0:0/124 and 127.0.0.0/28 are supported. Domain wildcards are not supported. To separate entries use commas, semicolons, or newlines. The allowlist can hold a maximum of 1000 entries. Domains must be IDNA encoded.')
= link_to _('Learn more.'), help_page_path('security/webhooks.md', anchor: 'allowlist-for-local-requests'), target: '_blank', rel: 'noopener noreferrer' = link_to _('Learn more.'), help_page_path('security/webhooks.md', anchor: 'create-an-allowlist-for-local-requests'), target: '_blank', rel: 'noopener noreferrer'
.form-group .form-group
= f.gitlab_ui_checkbox_component :dns_rebinding_protection_enabled, = f.gitlab_ui_checkbox_component :dns_rebinding_protection_enabled,

View File

@ -13,7 +13,6 @@
- application_performance - application_performance
- attack_emulation - attack_emulation
- audit_events - audit_events
- audit_reports
- authentication_and_authorization - authentication_and_authorization
- auto_devops - auto_devops
- backup_restore - backup_restore

View File

@ -1,8 +1,8 @@
--- ---
name: etag_merge_request_diff_batches name: ci_cost_factors_narrow_os_contribution_by_plan
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93953 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96273
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/369488 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/372263
milestone: '15.3' milestone: '15.4'
type: development type: development
group: group::code review group: group::pipeline execution
default_enabled: false default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: order_builds_for_group_runner
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94815
rollout_issue_url:
milestone: '15.4'
type: development
group: group::pipeline execution
default_enabled: true

View File

@ -1,7 +1,7 @@
--- ---
table_name: protected_environment_deploy_access_levels table_name: protected_environment_deploy_access_levels
classes: classes:
- ProtectedEnvironment::DeployAccessLevel - ProtectedEnvironments::DeployAccessLevel
feature_categories: feature_categories:
- continuous_delivery - continuous_delivery
description: https://docs.gitlab.com/ee/ci/environments/protected_environments.html description: https://docs.gitlab.com/ee/ci/environments/protected_environments.html

View File

@ -5,9 +5,9 @@
# #
# For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles # For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles
extends: existence extends: existence
message: 'Link "%s" must use the .md file extension.' message: 'Link "%s" must link directly to a file and use the .md file extension.'
link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#links-to-internal-documentation link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#links-to-internal-documentation
level: error level: error
scope: raw scope: raw
raw: raw:
- '\[.+\]\([\w\/\.-]+\.html[^)]*\)' - '\[[^\]]+\]\([^:\)]+(\/(#[^\)]+)?\)|\.html(#.+)?\))'

View File

@ -15,7 +15,7 @@ Read more about [group-level protected environments](../ci/environments/protecte
## Valid access levels ## Valid access levels
The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method. The access levels are defined in the `ProtectedEnvironments::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.
Currently, these levels are recognized: Currently, these levels are recognized:
```plaintext ```plaintext

View File

@ -11,7 +11,7 @@ type: concepts, howto
## Valid access levels ## Valid access levels
The access levels are defined in the `ProtectedEnvironment::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method. The access levels are defined in the `ProtectedEnvironments::DeployAccessLevel::ALLOWED_ACCESS_LEVELS` method.
Currently, these levels are recognized: Currently, these levels are recognized:
```plaintext ```plaintext

View File

@ -16,7 +16,7 @@ GitLab CI/CD supports [OpenID Connect (OIDC)](https://openid.net/connect/faq/) t
- Account on GitLab. - Account on GitLab.
- Access to a cloud provider that supports OIDC to configure authorization and create roles. - Access to a cloud provider that supports OIDC to configure authorization and create roles.
The original implementation of `CI_JOB_JWT` supports [HashiCorp Vault integration](../examples/authenticating-with-hashicorp-vault/). The updated implementation of `CI_JOB_JWT_V2` supports additional cloud providers with OIDC including AWS, Azure, GCP, and Vault. The original implementation of `CI_JOB_JWT` supports [HashiCorp Vault integration](../examples/authenticating-with-hashicorp-vault/index.md). The updated implementation of `CI_JOB_JWT_V2` supports additional cloud providers with OIDC including AWS, Azure, GCP, and Vault.
NOTE: NOTE:
Configuring OIDC enables JWT token access to the target environments for all pipelines. Configuring OIDC enables JWT token access to the target environments for all pipelines.

View File

@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Deployment safety **(FREE)** # Deployment safety **(FREE)**
[Deployment jobs](../jobs/#deployment-jobs) are a specific kind of CI/CD [Deployment jobs](../jobs/index.md#deployment-jobs) are a specific kind of CI/CD
job. They can be more sensitive than other jobs in a pipeline, job. They can be more sensitive than other jobs in a pipeline,
and might need to be treated with extra care. GitLab has several features and might need to be treated with extra care. GitLab has several features
that help maintain deployment security and stability. that help maintain deployment security and stability.
@ -66,7 +66,7 @@ For more information, see [Resource Group documentation](../resource_groups/inde
## Skip outdated deployment jobs ## Skip outdated deployment jobs
The effective execution order of pipeline jobs can vary from run to run, which The effective execution order of pipeline jobs can vary from run to run, which
could cause undesired behavior. For example, a [deployment job](../jobs/#deployment-jobs) could cause undesired behavior. For example, a [deployment job](../jobs/index.md#deployment-jobs)
in a newer pipeline could finish before a deployment job in an older pipeline. in a newer pipeline could finish before a deployment job in an older pipeline.
This creates a race condition where the older deployment finishes later, This creates a race condition where the older deployment finishes later,
overwriting the "newer" deployment. overwriting the "newer" deployment.

View File

@ -156,7 +156,7 @@ If parsing JUnit report XML results in an error, an indicator is shown next to t
![Test Reports With Errors](img/pipelines_junit_test_report_with_errors_v13_10.png) ![Test Reports With Errors](img/pipelines_junit_test_report_with_errors_v13_10.png)
For test case parsing limits, see [Max test cases per unit test report](../../user/gitlab_com/#gitlab-cicd). For test case parsing limits, see [Max test cases per unit test report](../../user/gitlab_com/index.md#gitlab-cicd).
GitLab does not parse very [large nodes](https://nokogiri.org/tutorials/parsing_an_html_xml_document.html#parse-options) of JUnit reports. There is [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/268035) open to make this optional. GitLab does not parse very [large nodes](https://nokogiri.org/tutorials/parsing_an_html_xml_document.html#parse-options) of JUnit reports. There is [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/268035) open to make this optional.

View File

@ -169,7 +169,7 @@ GitLab can display the results of one or more reports in:
- The pipeline [**Security** tab](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline). - The pipeline [**Security** tab](../../user/application_security/vulnerability_report/pipeline.md#view-vulnerabilities-in-a-pipeline).
- The [security dashboard](../../user/application_security/security_dashboard/index.md). - The [security dashboard](../../user/application_security/security_dashboard/index.md).
- The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md). - The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md).
- The [dependency list](../../user/application_security/dependency_list/). - The [dependency list](../../user/application_security/dependency_list/index.md).
## `artifacts:reports:dotenv` ## `artifacts:reports:dotenv`

View File

@ -177,7 +177,7 @@ If you get this error message while configuring GitLab, the following are possib
- GitLab is unable to reach your Jenkins instance at the address. If your GitLab instance is self-managed, try pinging the - GitLab is unable to reach your Jenkins instance at the address. If your GitLab instance is self-managed, try pinging the
Jenkins instance at the domain provided on the GitLab instance. Jenkins instance at the domain provided on the GitLab instance.
- The Jenkins instance is at a local address and is not included in the - The Jenkins instance is at a local address and is not included in the
[GitLab installation's allowlist](../security/webhooks.md#allowlist-for-local-requests). [GitLab installation's allowlist](../security/webhooks.md#create-an-allowlist-for-local-requests).
- The credentials for the Jenkins instance do not have sufficient access or are invalid. - The credentials for the Jenkins instance do not have sufficient access or are invalid.
- The **Enable authentication for `/project` end-point** checkbox is not selected in your [Jenkin's plugin configuration](#configure-the-jenkins-server). - The **Enable authentication for `/project` end-point** checkbox is not selected in your [Jenkin's plugin configuration](#configure-the-jenkins-server).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@ -7,78 +7,80 @@ type: concepts, reference, howto
# Webhooks and insecure internal web services **(FREE SELF)** # Webhooks and insecure internal web services **(FREE SELF)**
Users with at least the Maintainer role can set up [Webhooks](../user/project/integrations/webhooks.md) that are Users with at least the Maintainer role can set up [webhooks](../user/project/integrations/webhooks.md) that are
triggered when specific changes occur in a project. When triggered, a `POST` HTTP request is sent to a URL. A webhook is triggered when specific changes occur in a project. When triggered, a `POST` HTTP request is sent to a URL. A webhook is
usually configured to send data to a specific external web service, which usually configured to send data to a specific external web service, which processes the data in an appropriate way.
processes the data in an appropriate way.
However, a Webhook can be configured with a URL for an internal web service instead of an external web service. However, a webhook can be configured with a URL for an internal web service instead of an external web service.
When the Webhook is triggered, When the webhook is triggered, non-GitLab web services running on your GitLab server or in its local network could be
non-GitLab web services running on your GitLab server or in its local network could be exploited. exploited.
Webhook requests are made by the GitLab server itself and use a single Webhook requests are made by the GitLab server itself and use a single optional secret token per hook for authorization
(optional) secret token per hook for authorization (instead of a user or instead of:
repository-specific token). As a result, these requests may have broader access than
intended, including access to everything running on the server hosting the webhook. This
may include the GitLab server or API itself (for example, `http://localhost:123`).
Depending on the called webhook, this may also result in network access
to other servers within that webhook server's local network (for example,
`http://192.168.1.12:345`), even if these services are otherwise protected
and inaccessible from the outside world.
If a web service does not require authentication, Webhooks can be used to - A user token.
trigger destructive commands by getting the GitLab server to make POST requests - A repository-specific token.
to endpoints like `http://localhost:123/some-resource/delete`.
## Allow requests to local network As a result, these requests can have broader access than intended, including access to everything running on the server
that hosts the webhook including:
To prevent this type of exploitation from happening, starting with GitLab 10.6, - The GitLab server.
all Webhook requests to the current GitLab instance server address and/or in a - The API itself.
private network are forbidden by default. That means that all requests made - For some webhooks, network access to other servers in that webhook server's local network, even if these services
to `127.0.0.1`, `::1` and `0.0.0.0`, as well as IPv4 `10.0.0.0/8`, `172.16.0.0/12`, are otherwise protected and inaccessible from the outside world.
`192.168.0.0/16` and IPv6 site-local (`ffc0::/10`) addresses aren't allowed.
This behavior can be overridden: Webhooks can be used to trigger destructive commands using web services that don't require authentication. These webhooks
can get the GitLab server to make `POST` HTTP requests to endpoints that delete resources.
## Allow webhook and service requests to local network
To prevent exploitation of insecure internal web services, all webhook requests to the following local network addresses are not allowed:
- The current GitLab instance server address.
- Private network addresses, including `127.0.0.1`, `::1`, `0.0.0.0`, `10.0.0.0/8`, `172.16.0.0/12`,
`192.168.0.0/16`, and IPv6 site-local (`ffc0::/10`) addresses.
To allow access to these addresses:
1. On the top bar, select **Menu > Admin**. 1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > Network**. 1. On the left sidebar, select **Settings > Network**.
1. Expand the **Outbound requests** section. 1. Expand **Outbound requests**.
1. Select **Allow requests to the local network from web hooks and services**. 1. Select the **Allow requests to the local network from web hooks and services** checkbox.
NOTE: ## Prevent system hook requests to local network
*System hooks* are enabled to make requests to local network by default since they are
set up by administrators. However, you can turn this off by disabling the
**Allow requests to the local network from system hooks** option.
## Allowlist for local requests [System hooks](../administration/system_hooks.md) are permitted to make requests to local network by default because
they are set up by administrators. To prevent system hook requests to the local network:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > Network**.
1. Expand **Outbound requests**.
1. Clear the **Allow requests to the local network from system hooks** checkbox.
## Create an allowlist for local requests
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/44496) in GitLab 12.2 > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/44496) in GitLab 12.2
You can allow certain domains and IP addresses to be accessible to both *system hooks* You can allow certain domains and IP addresses to be accessible to both system hooks and webhooks, even when local
and *webhooks* even when local requests are not allowed by adding them to the requests are forbidden. To add these domains to the allowlist:
allowlist:
1. On the top bar, select **Menu > Admin**. 1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > Network** (`/admin/application_settings/network`) 1. On the left sidebar, select **Settings > Network**.
and expand **Outbound requests**: 1. Expand **Outbound requests** and add entries.
![Outbound local requests allowlist](img/allowlist_v13_0.png) The entries can:
The allowed entries can be separated by semicolons, commas or whitespaces - Be separated by semicolons, commas, or whitespaces (including newlines).
(including newlines) and be in different formats like hostnames, IP addresses and/or - Be in different formats like hostnames, IP addresses, IP address ranges. IPv6 is supported. Hostnames that contain
IP ranges. IPv6 is supported. Hostnames that contain Unicode characters should Unicode characters should use [Internationalized Domain Names in Applications](https://www.icann.org/en/icann-acronyms-and-terms/internationalized-domain-names-in-applications-en)
use [Internationalized Domain Names in Applications](https://www.icann.org/en/icann-acronyms-and-terms/internationalized-domain-names-in-applications-en) (IDNA) encoding.
(IDNA) encoding. - Include ports. For example, `127.0.0.1:8080` only allows connections to port 8080 on `127.0.0.1`. If no port is specified,
all ports on that IP address or domain are allowed. An IP address range allows all ports on all IP addresses in that
range.
- Number no more than 1000 entries of no more than 255 characters for each entry.
- Not contain wildcards (for example, `*.example.com`).
The allowlist can hold a maximum of 1000 entries. Each entry can be a maximum of For example:
255 characters.
You can allow a particular port by specifying it in the allowlist entry.
For example `127.0.0.1:8080` only allows connections to port 8080 on `127.0.0.1`.
If no port is mentioned, all ports on that IP/domain are allowed. An IP range
allows all ports on all IPs in that range.
Example:
```plaintext ```plaintext
example.com;gitlab.example.com example.com;gitlab.example.com
@ -89,9 +91,6 @@ example.com;gitlab.example.com
example.com:8080 example.com:8080
``` ```
NOTE:
Wildcards (`*.example.com`) are not currently supported.
<!-- ## Troubleshooting <!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues Include any troubleshooting steps that you can foresee. If you know beforehand what issues

View File

@ -26,7 +26,7 @@ If you are importing from GitHub Enterprise to a self-managed GitLab instance:
- You must first enable [GitHub integration](../../../integration/github.md). - You must first enable [GitHub integration](../../../integration/github.md).
- To import projects from GitHub Enterprise to GitLab.com, use the [Import API](../../../api/import.md). - To import projects from GitHub Enterprise to GitLab.com, use the [Import API](../../../api/import.md).
- If GitLab is behind a HTTP/HTTPS proxy, you must populate the [allowlist for local requests](../../../security/webhooks.md#allowlist-for-local-requests) - If GitLab is behind a HTTP/HTTPS proxy, you must populate the [allowlist for local requests](../../../security/webhooks.md#create-an-allowlist-for-local-requests)
with `github.com` and `api.github.com` to solve the hostname. For more information, read the issue with `github.com` and `api.github.com` to solve the hostname. For more information, read the issue
[Importing a GitHub project requires DNS resolution even when behind a proxy](https://gitlab.com/gitlab-org/gitlab/-/issues/37941). [Importing a GitHub project requires DNS resolution even when behind a proxy](https://gitlab.com/gitlab-org/gitlab/-/issues/37941).

View File

@ -2,11 +2,22 @@
module API module API
class RpmProjectPackages < ::API::Base class RpmProjectPackages < ::API::Base
helpers ::API::Helpers::PackagesHelpers helpers ::API::Helpers::PackagesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers
include ::API::Helpers::Authentication
feature_category :package_registry feature_category :package_registry
before do before do
require_packages_enabled! require_packages_enabled!
not_found! unless Feature.enabled?(:rpm_packages)
not_found! unless ::Feature.enabled?(:rpm_packages, authorized_user_project)
authorize_read_package!(authorized_user_project)
end
authenticate_with do |accept|
accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username)
.sent_through(:http_basic_auth)
end end
params do params do
@ -18,7 +29,7 @@ module API
params do params do
requires :file_name, type: String, desc: 'Repository metadata file name' requires :file_name, type: String, desc: 'Repository metadata file name'
end end
get 'repodata/*file_name' do get 'repodata/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do
not_found! not_found!
end end
@ -27,12 +38,13 @@ module API
requires :package_file_id, type: Integer, desc: 'RPM package file id' requires :package_file_id, type: Integer, desc: 'RPM package file id'
requires :file_name, type: String, desc: 'RPM package file name' requires :file_name, type: String, desc: 'RPM package file name'
end end
get '*package_file_id/*file_name' do get '*package_file_id/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do
not_found! not_found!
end end
desc 'Upload a RPM package' desc 'Upload a RPM package'
post do post do
authorize_create_package!(authorized_user_project)
not_found! not_found!
end end

View File

@ -9391,6 +9391,9 @@ msgstr ""
msgid "Collapse jobs" msgid "Collapse jobs"
msgstr "" msgstr ""
msgid "Collapse merge details"
msgstr ""
msgid "Collapse milestones" msgid "Collapse milestones"
msgstr "" msgstr ""
@ -12502,6 +12505,9 @@ msgstr ""
msgid "Delete deploy key" msgid "Delete deploy key"
msgstr "" msgstr ""
msgid "Delete epic"
msgstr ""
msgid "Delete file" msgid "Delete file"
msgstr "" msgstr ""
@ -14986,6 +14992,9 @@ msgstr ""
msgid "Epic Boards" msgid "Epic Boards"
msgstr "" msgstr ""
msgid "Epic actions"
msgstr ""
msgid "Epic cannot be found." msgid "Epic cannot be found."
msgstr "" msgstr ""
@ -15687,6 +15696,9 @@ msgstr ""
msgid "Expand jobs" msgid "Expand jobs"
msgstr "" msgstr ""
msgid "Expand merge details"
msgstr ""
msgid "Expand milestones" msgid "Expand milestones"
msgstr "" msgstr ""

View File

@ -14,7 +14,7 @@
"jest": "jest --config jest.config.js", "jest": "jest --config jest.config.js",
"jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand", "jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js", "jest:ci": "jest --config jest.config.js --ci --coverage --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
"jest:ci:minimal": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_MATCHING_TESTS_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js", "jest:ci:minimal": "jest --config jest.config.js --ci --coverage --findRelatedTests $(cat $RSPEC_CHANGED_FILES_PATH) --passWithNoTests --testSequencer ./scripts/frontend/parallel_ci_sequencer.js",
"jest:integration": "jest --config jest.config.integration.js", "jest:integration": "jest --config jest.config.integration.js",
"lint:eslint": "node scripts/frontend/eslint.js", "lint:eslint": "node scripts/frontend/eslint.js",
"lint:eslint:fix": "node scripts/frontend/eslint.js --fix", "lint:eslint:fix": "node scripts/frontend/eslint.js --fix",

View File

@ -2,16 +2,9 @@ import { GlButton } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssuableEditActions from '~/issues/show/components/edit_actions.vue'; import IssuableEditActions from '~/issues/show/components/edit_actions.vue';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import eventHub from '~/issues/show/event_hub'; import eventHub from '~/issues/show/event_hub';
import {
getIssueStateQueryResponse,
updateIssueStateQueryResponse,
} from '../mock_data/apollo_mock';
describe('Edit Actions component', () => { describe('Edit Actions component', () => {
let wrapper; let wrapper;
@ -31,8 +24,6 @@ describe('Edit Actions component', () => {
}, },
}; };
const modalId = 'delete-issuable-modal-1';
const createComponent = ({ props, data } = {}) => { const createComponent = ({ props, data } = {}) => {
fakeApollo = createMockApollo([], mockResolvers); fakeApollo = createMockApollo([], mockResolvers);
@ -50,16 +41,13 @@ describe('Edit Actions component', () => {
data() { data() {
return { return {
issueState: {}, issueState: {},
modalId,
...data, ...data,
}; };
}, },
}); });
}; };
const findModal = () => wrapper.findComponent(DeleteIssueModal);
const findEditButtons = () => wrapper.findAllComponents(GlButton); const findEditButtons = () => wrapper.findAllComponents(GlButton);
const findDeleteButton = () => wrapper.findByTestId('issuable-delete-button');
const findSaveButton = () => wrapper.findByTestId('issuable-save-button'); const findSaveButton = () => wrapper.findByTestId('issuable-save-button');
const findCancelButton = () => wrapper.findByTestId('issuable-cancel-button'); const findCancelButton = () => wrapper.findByTestId('issuable-cancel-button');
@ -79,23 +67,12 @@ describe('Edit Actions component', () => {
}); });
}); });
it('does not render the delete button if canDestroy is false', () => {
createComponent({ props: { canDestroy: false } });
expect(findDeleteButton().exists()).toBe(false);
});
it('disables save button when title is blank', () => { it('disables save button when title is blank', () => {
createComponent({ props: { formState: { title: '', issue_type: '' } } }); createComponent({ props: { formState: { title: '', issue_type: '' } } });
expect(findSaveButton().attributes('disabled')).toBe('true'); expect(findSaveButton().attributes('disabled')).toBe('true');
}); });
it('does not render the delete button if showDeleteButton is false', () => {
createComponent({ props: { showDeleteButton: false } });
expect(findDeleteButton().exists()).toBe(false);
});
describe('updateIssuable', () => { describe('updateIssuable', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
@ -119,63 +96,4 @@ describe('Edit Actions component', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); expect(eventHub.$emit).toHaveBeenCalledWith('close.form');
}); });
}); });
describe('delete issue button', () => {
let trackingSpy;
beforeEach(() => {
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('tracks clicking on button', () => {
findDeleteButton().vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_button', {
label: 'delete_issue',
});
});
});
describe('delete issue modal', () => {
it('renders', () => {
expect(findModal().props()).toEqual({
issuePath: 'gitlab-org/gitlab-test/-/issues/1',
issueType: 'Issue',
modalId,
title: 'Delete issue',
});
});
});
describe('deleteIssuable', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
it('does not send the `delete.issuable` event when clicking delete button', () => {
findDeleteButton().vm.$emit('click');
expect(eventHub.$emit).not.toHaveBeenCalled();
});
it('sends the `delete.issuable` event when clicking the delete confirm button', async () => {
expect(eventHub.$emit).toHaveBeenCalledTimes(0);
findModal().vm.$emit('delete');
expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable');
expect(eventHub.$emit).toHaveBeenCalledTimes(1);
});
});
describe('with Apollo cache mock', () => {
it('renders the right delete button text per apollo cache type', async () => {
mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
await waitForPromises();
expect(findDeleteButton().text()).toBe('Delete issue');
});
it('should not change the delete button text per apollo cache mutation', async () => {
mockIssueStateData.mockResolvedValue(updateIssueStateQueryResponse);
await waitForPromises();
expect(findDeleteButton().text()).toBe('Delete issue');
});
});
}); });

View File

@ -41,106 +41,135 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
</div> </div>
<div <div
class="media-body gl-display-flex" class="gl-display-flex gl-w-full"
> >
<h4
class="gl-mr-3"
data-testid="statusText"
>
Set by
<a
class="author-link inline"
>
<img
class="avatar avatar-inline s16"
src="no_avatar.png"
/>
<span
class="author"
>
</span>
</a>
to be merged automatically when the pipeline succeeds
</h4>
<div <div
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto" class="media-body gl-display-flex"
> >
<div>
<div <h4
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" class="gl-mr-3"
lazy="" data-testid="statusText"
no-caret="" >
Set by
<a
class="author-link inline"
> >
<!----> <img
class="avatar avatar-inline s16"
src="no_avatar.png"
/>
<span
class="author"
>
</span>
</a>
to be merged automatically when the pipeline succeeds
</h4>
<div
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
>
<div>
<div
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
lazy=""
no-caret=""
>
<!---->
<button
aria-expanded="false"
aria-haspopup="true"
class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
type="button"
>
<!---->
<svg
aria-hidden="true"
class="dropdown-icon gl-icon s16"
data-testid="ellipsis_v-icon"
role="img"
>
<use
href="#ellipsis_v"
/>
</svg>
<span
class="gl-new-dropdown-button-text gl-sr-only"
>
</span>
<svg
aria-hidden="true"
class="gl-button-icon dropdown-chevron gl-icon s16"
data-testid="chevron-down-icon"
role="img"
>
<use
href="#chevron-down"
/>
</svg>
</button>
<ul
class="dropdown-menu dropdown-menu-right"
role="menu"
tabindex="-1"
>
<!---->
</ul>
</div>
<button <button
aria-expanded="false" class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
aria-haspopup="true" data-qa-selector="cancel_auto_merge_button"
class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" data-testid="cancelAutomaticMergeButton"
type="button" type="button"
> >
<!----> <!---->
<svg <!---->
aria-hidden="true"
class="dropdown-icon gl-icon s16"
data-testid="ellipsis_v-icon"
role="img"
>
<use
href="#ellipsis_v"
/>
</svg>
<span <span
class="gl-new-dropdown-button-text gl-sr-only" class="gl-button-text"
> >
</span>
<svg
aria-hidden="true"
class="gl-button-icon dropdown-chevron gl-icon s16"
data-testid="chevron-down-icon"
role="img"
>
<use
href="#chevron-down"
/>
</svg>
</button>
<ul
class="dropdown-menu dropdown-menu-right"
role="menu"
tabindex="-1"
>
<!---->
</ul>
</div>
<button
class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Cancel auto-merge Cancel auto-merge
</span> </span>
</button> </button>
</div>
</div> </div>
</div> </div>
<div
class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
>
<button
class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
title="Collapse merge details"
type="button"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="chevron-lg-up-icon"
role="img"
>
<use
href="#chevron-lg-up"
/>
</svg>
<!---->
</button>
</div>
</div> </div>
</div> </div>
`; `;
@ -186,106 +215,135 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
</div> </div>
<div <div
class="media-body gl-display-flex" class="gl-display-flex gl-w-full"
> >
<h4
class="gl-mr-3"
data-testid="statusText"
>
Set by
<a
class="author-link inline"
>
<img
class="avatar avatar-inline s16"
src="no_avatar.png"
/>
<span
class="author"
>
</span>
</a>
to be merged automatically when the pipeline succeeds
</h4>
<div <div
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto" class="media-body gl-display-flex"
> >
<div>
<div <h4
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group" class="gl-mr-3"
lazy="" data-testid="statusText"
no-caret="" >
Set by
<a
class="author-link inline"
> >
<!----> <img
class="avatar avatar-inline s16"
src="no_avatar.png"
/>
<span
class="author"
>
</span>
</a>
to be merged automatically when the pipeline succeeds
</h4>
<div
class="gl-display-flex gl-md-display-block gl-font-size-0 gl-ml-auto"
>
<div>
<div
class="dropdown b-dropdown gl-new-dropdown gl-display-block gl-md-display-none! btn-group"
lazy=""
no-caret=""
>
<!---->
<button
aria-expanded="false"
aria-haspopup="true"
class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
type="button"
>
<!---->
<svg
aria-hidden="true"
class="dropdown-icon gl-icon s16"
data-testid="ellipsis_v-icon"
role="img"
>
<use
href="#ellipsis_v"
/>
</svg>
<span
class="gl-new-dropdown-button-text gl-sr-only"
>
</span>
<svg
aria-hidden="true"
class="gl-button-icon dropdown-chevron gl-icon s16"
data-testid="chevron-down-icon"
role="img"
>
<use
href="#chevron-down"
/>
</svg>
</button>
<ul
class="dropdown-menu dropdown-menu-right"
role="menu"
tabindex="-1"
>
<!---->
</ul>
</div>
<button <button
aria-expanded="false" class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
aria-haspopup="true" data-qa-selector="cancel_auto_merge_button"
class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret" data-testid="cancelAutomaticMergeButton"
type="button" type="button"
> >
<!----> <!---->
<svg <!---->
aria-hidden="true"
class="dropdown-icon gl-icon s16"
data-testid="ellipsis_v-icon"
role="img"
>
<use
href="#ellipsis_v"
/>
</svg>
<span <span
class="gl-new-dropdown-button-text gl-sr-only" class="gl-button-text"
> >
</span>
<svg
aria-hidden="true"
class="gl-button-icon dropdown-chevron gl-icon s16"
data-testid="chevron-down-icon"
role="img"
>
<use
href="#chevron-down"
/>
</svg>
</button>
<ul
class="dropdown-menu dropdown-menu-right"
role="menu"
tabindex="-1"
>
<!---->
</ul>
</div>
<button
class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton"
type="button"
>
<!---->
<!---->
<span
class="gl-button-text"
>
Cancel auto-merge Cancel auto-merge
</span> </span>
</button> </button>
</div>
</div> </div>
</div> </div>
<div
class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
>
<button
class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
title="Collapse merge details"
type="button"
>
<!---->
<svg
aria-hidden="true"
class="gl-button-icon gl-icon s16"
data-testid="chevron-lg-up-icon"
role="img"
>
<use
href="#chevron-lg-up"
/>
</svg>
<!---->
</button>
</div>
</div> </div>
</div> </div>
`; `;

View File

@ -6,7 +6,7 @@ describe('MRWidgetArchived', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(archivedComponent); wrapper = shallowMount(archivedComponent, { propsData: { mr: {} } });
}); });
afterEach(() => { afterEach(() => {

View File

@ -6,7 +6,7 @@ describe('MRWidgetChecking', () => {
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(CheckingComponent); wrapper = shallowMount(CheckingComponent, { propsData: { mr: {} } });
}); });
afterEach(() => { afterEach(() => {

View File

@ -0,0 +1,122 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
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 { workItemTask, confidentialWorkItemTask, closedWorkItemTask } from '../../mock_data';
describe('WorkItemLinkChild', () => {
const WORK_ITEM_ID = 'gid://gitlab/WorkItem/2';
let wrapper;
const createComponent = ({
projectPath = 'gitlab-org/gitlab-test',
canUpdate = true,
issuableGid = WORK_ITEM_ID,
childItem = workItemTask,
} = {}) => {
wrapper = shallowMountExtended(WorkItemLinkChild, {
propsData: {
projectPath,
canUpdate,
issuableGid,
childItem,
},
});
};
afterEach(() => {
wrapper.destroy();
});
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.findComponent(GlButton);
});
it('renders item title', () => {
expect(titleEl.attributes('href')).toBe('/gitlab-org/gitlab-test/-/work_items/4');
expect(titleEl.text()).toBe(workItemTask.title);
});
it.each`
action | event | emittedEvent
${'clicking'} | ${'click'} | ${'click'}
${'doing mouseover on'} | ${'mouseover'} | ${'mouseover'}
${'doing mouseout on'} | ${'mouseout'} | ${'mouseout'}
`('$action item title emit `$emittedEvent` event', ({ event, emittedEvent }) => {
const eventObj = {
preventDefault: jest.fn(),
};
titleEl.vm.$emit(event, eventObj);
expect(wrapper.emitted(emittedEvent)).toEqual([[workItemTask.id, eventObj]]);
});
});
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('remove')).toEqual([[workItemTask.id]]);
});
});
});

View File

@ -1,5 +1,5 @@
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import { GlButton, GlIcon, GlAlert } from '@gitlab/ui'; import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
@ -7,6 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql'; import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue'; import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
@ -92,10 +93,10 @@ describe('WorkItemLinks', () => {
const findLinksBody = () => wrapper.findByTestId('links-body'); const findLinksBody = () => wrapper.findByTestId('links-body');
const findEmptyState = () => wrapper.findByTestId('links-empty'); const findEmptyState = () => wrapper.findByTestId('links-empty');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form'); const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
const findWorkItemLinkChildItems = () => wrapper.findAllComponents(WorkItemLinkChild);
const findFirstWorkItemLinkChild = () => findWorkItemLinkChildItems().at(0);
const findAddLinksForm = () => wrapper.findByTestId('add-links-form'); const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
const findFirstLinksMenu = () => wrapper.findByTestId('links-menu');
const findChildrenCount = () => wrapper.findByTestId('children-count'); const findChildrenCount = () => wrapper.findByTestId('children-count');
const findChildren = () => wrapper.findAllByTestId('links-child');
beforeEach(async () => { beforeEach(async () => {
await createComponent(); await createComponent();
@ -148,8 +149,7 @@ describe('WorkItemLinks', () => {
it('renders all hierarchy widget children', () => { it('renders all hierarchy widget children', () => {
expect(findLinksBody().exists()).toBe(true); expect(findLinksBody().exists()).toBe(true);
expect(findChildren()).toHaveLength(4); expect(findWorkItemLinkChildItems()).toHaveLength(4);
expect(findFirstLinksMenu().exists()).toBe(true);
}); });
it('shows alert when list loading fails', async () => { it('shows alert when list loading fails', async () => {
@ -164,19 +164,6 @@ describe('WorkItemLinks', () => {
expect(findAlert().text()).toBe(errorMessage); expect(findAlert().text()).toBe(errorMessage);
}); });
it('renders widget child icon and tooltip', () => {
expect(findChildren().at(0).findComponent(GlIcon).props('name')).toBe('issue-open-m');
expect(findChildren().at(1).findComponent(GlIcon).props('name')).toBe('issue-close');
});
it('renders confidentiality icon when child item is confidential', () => {
const children = wrapper.findAll('[data-testid="links-child"]');
const confidentialIcon = children.at(0).find('[data-testid="confidential-icon"]');
expect(confidentialIcon.exists()).toBe(true);
expect(confidentialIcon.props('name')).toBe('eye-slash');
});
it('displays number if children', () => { it('displays number if children', () => {
expect(findChildrenCount().exists()).toBe(true); expect(findChildrenCount().exists()).toBe(true);
@ -195,17 +182,21 @@ describe('WorkItemLinks', () => {
}); });
it('does not display link menu on children', () => { it('does not display link menu on children', () => {
expect(findFirstLinksMenu().exists()).toBe(false); expect(findWorkItemLinkChildItems().at(0).props('canUpdate')).toBe(false);
}); });
}); });
describe('remove child', () => { describe('remove child', () => {
let firstChild;
beforeEach(async () => { beforeEach(async () => {
await createComponent({ mutationHandler: mutationChangeParentHandler }); await createComponent({ mutationHandler: mutationChangeParentHandler });
firstChild = findFirstWorkItemLinkChild();
}); });
it('calls correct mutation with correct variables', async () => { it('calls correct mutation with correct variables', async () => {
findFirstLinksMenu().vm.$emit('removeChild'); firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
await waitForPromises(); await waitForPromises();
@ -220,7 +211,7 @@ describe('WorkItemLinks', () => {
}); });
it('shows toast when mutation succeeds', async () => { it('shows toast when mutation succeeds', async () => {
findFirstLinksMenu().vm.$emit('removeChild'); firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
await waitForPromises(); await waitForPromises();
@ -230,28 +221,30 @@ describe('WorkItemLinks', () => {
}); });
it('renders correct number of children after removal', async () => { it('renders correct number of children after removal', async () => {
expect(findChildren()).toHaveLength(4); expect(findWorkItemLinkChildItems()).toHaveLength(4);
findFirstLinksMenu().vm.$emit('removeChild'); firstChild.vm.$emit('remove', firstChild.vm.childItem.id);
await waitForPromises(); await waitForPromises();
expect(findChildren()).toHaveLength(3); expect(findWorkItemLinkChildItems()).toHaveLength(3);
}); });
}); });
describe('prefetching child items', () => { describe('prefetching child items', () => {
let firstChild;
beforeEach(async () => { beforeEach(async () => {
await createComponent(); await createComponent();
});
const findChildLink = () => findChildren().at(0).findComponent(GlButton); firstChild = findFirstWorkItemLinkChild();
});
it('does not fetch the child work item before hovering work item links', () => { it('does not fetch the child work item before hovering work item links', () => {
expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); expect(childWorkItemQueryHandler).not.toHaveBeenCalled();
}); });
it('fetches the child work item if link is hovered for 250+ ms', async () => { it('fetches the child work item if link is hovered for 250+ ms', async () => {
findChildLink().vm.$emit('mouseover'); firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS); jest.advanceTimersByTime(DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
await waitForPromises(); await waitForPromises();
@ -261,9 +254,9 @@ describe('WorkItemLinks', () => {
}); });
it('does not fetch the child work item if link is hovered for less than 250 ms', async () => { it('does not fetch the child work item if link is hovered for less than 250 ms', async () => {
findChildLink().vm.$emit('mouseover'); firstChild.vm.$emit('mouseover', firstChild.vm.childItem.id);
jest.advanceTimersByTime(200); jest.advanceTimersByTime(200);
findChildLink().vm.$emit('mouseout'); firstChild.vm.$emit('mouseout', firstChild.vm.childItem.id);
await waitForPromises(); await waitForPromises();
expect(childWorkItemQueryHandler).not.toHaveBeenCalled(); expect(childWorkItemQueryHandler).not.toHaveBeenCalled();

View File

@ -515,6 +515,48 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
}, },
}; };
export const workItemTask = {
id: 'gid://gitlab/WorkItem/4',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
__typename: 'WorkItemType',
},
title: 'bar',
state: 'OPEN',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
__typename: 'WorkItem',
};
export const confidentialWorkItemTask = {
id: 'gid://gitlab/WorkItem/2',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
__typename: 'WorkItemType',
},
title: 'xyz',
state: 'OPEN',
confidential: true,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
__typename: 'WorkItem',
};
export const closedWorkItemTask = {
id: 'gid://gitlab/WorkItem/3',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
__typename: 'WorkItemType',
},
title: 'abc',
state: 'CLOSED',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: '2022-08-12T13:07:52Z',
__typename: 'WorkItem',
};
export const workItemHierarchyResponse = { export const workItemHierarchyResponse = {
data: { data: {
workItem: { workItem: {
@ -544,45 +586,9 @@ export const workItemHierarchyResponse = {
parent: null, parent: null,
children: { children: {
nodes: [ nodes: [
{ confidentialWorkItemTask,
id: 'gid://gitlab/WorkItem/2', closedWorkItemTask,
workItemType: { workItemTask,
id: 'gid://gitlab/WorkItems::Type/5',
__typename: 'WorkItemType',
},
title: 'xyz',
state: 'OPEN',
confidential: true,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/3',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
__typename: 'WorkItemType',
},
title: 'abc',
state: 'CLOSED',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: '2022-08-12T13:07:52Z',
__typename: 'WorkItem',
},
{
id: 'gid://gitlab/WorkItem/4',
workItemType: {
id: 'gid://gitlab/WorkItems::Type/5',
__typename: 'WorkItemType',
},
title: 'bar',
state: 'OPEN',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
__typename: 'WorkItem',
},
{ {
id: 'gid://gitlab/WorkItem/5', id: 'gid://gitlab/WorkItem/5',
workItemType: { workItemType: {

View File

@ -706,7 +706,7 @@ ProtectedEnvironment:
- name - name
- created_at - created_at
- updated_at - updated_at
ProtectedEnvironment::DeployAccessLevel: ProtectedEnvironments::DeployAccessLevel:
- id - id
- protected_environment_id - protected_environment_id
- access_level - access_level

View File

@ -2,13 +2,194 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe API::RpmProjectPackages do RSpec.describe API::RpmProjectPackages do
include PackagesManagerApiSpecHelpers include HttpBasicAuthHelpers
let(:project) { create(:project) }
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let_it_be(:job) { create(:ci_build, :running, user: user, project: project) }
let(:headers) { {} } let(:headers) { {} }
let(:package_name) { 'rpm-package.0-1.x86_64.rpm' } let(:package_name) { 'rpm-package.0-1.x86_64.rpm' }
let(:package_file_id) { 1 } let(:package_file_id) { 1 }
shared_examples 'an unimplemented route' do shared_examples 'rejects rpm packages access' do |status|
it_behaves_like 'returning response status', status
if status == :unauthorized
it 'has the correct response header' do
subject
expect(response.headers['WWW-Authenticate']).to eq 'Basic realm="GitLab Packages Registry"'
end
end
end
shared_examples 'process rpm packages upload/download' do |status|
it_behaves_like 'returning response status', status
end
shared_examples 'a deploy token for RPM requests' do
context 'with deploy token headers' do
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
end
let(:headers) { basic_auth_header(deploy_token.username, deploy_token.token) }
context 'when token is valid' do
it_behaves_like 'returning response status', :not_found
end
context 'when token is invalid' do
let(:headers) { basic_auth_header(deploy_token.username, 'bar') }
it_behaves_like 'returning response status', :unauthorized
end
end
end
shared_examples 'a job token for RPM requests' do
context 'with job token headers' do
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
project.add_developer(user)
end
context 'with valid token' do
it_behaves_like 'returning response status', :not_found
end
context 'with invalid token' do
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
it_behaves_like 'returning response status', :unauthorized
end
context 'with invalid user' do
let(:headers) { basic_auth_header('foo', job.token) }
it_behaves_like 'returning response status', :unauthorized
end
end
end
shared_examples 'a user token for RPM requests' do
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
'PUBLIC' | :guest | true | true | 'process rpm packages upload/download' | :forbidden
'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'process rpm packages upload/download' | :not_found
'PUBLIC' | :guest | false | true | 'process rpm packages upload/download' | :not_found
'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'process rpm packages upload/download' | :unauthorized
'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
end
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
subject { get api(url), headers: headers }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level))
project.send("add_#{user_role}", user) if member && user_role != :anonymous
end
it_behaves_like params[:shared_examples_name], params[:expected_status]
end
end
end
describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do
let(:url) { "/projects/#{project.id}/packages/rpm/repodata/#{package_name}" }
subject { get api(url), headers: headers }
it_behaves_like 'a job token for RPM requests'
it_behaves_like 'a deploy token for RPM requests'
it_behaves_like 'a user token for RPM requests'
end
describe 'GET /api/v4/projects/:id/packages/rpm/:package_file_id/:filename' do
let(:url) { "/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}" }
subject { get api(url), headers: headers }
it_behaves_like 'a job token for RPM requests'
it_behaves_like 'a deploy token for RPM requests'
it_behaves_like 'a user token for RPM requests'
end
describe 'POST /api/v4/projects/:project_id/packages/rpm' do
let(:url) { "/projects/#{project.id}/packages/rpm" }
subject { post api(url), headers: headers }
context 'with user token' do
context 'with valid project' do
where(:visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do
'PUBLIC' | :developer | true | true | 'process rpm packages upload/download' | :not_found
'PUBLIC' | :guest | true | true | 'rejects rpm packages access' | :forbidden
'PUBLIC' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :developer | false | true | 'rejects rpm packages access' | :not_found
'PUBLIC' | :guest | false | true | 'rejects rpm packages access' | :not_found
'PUBLIC' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
'PUBLIC' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :developer | true | true | 'process rpm packages upload/download' | :not_found
'PRIVATE' | :guest | true | true | 'rejects rpm packages access' | :forbidden
'PRIVATE' | :developer | true | false | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :guest | true | false | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :developer | false | true | 'rejects rpm packages access' | :not_found
'PRIVATE' | :guest | false | true | 'rejects rpm packages access' | :not_found
'PRIVATE' | :developer | false | false | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :guest | false | false | 'rejects rpm packages access' | :unauthorized
'PRIVATE' | :anonymous | false | true | 'rejects rpm packages access' | :unauthorized
end
with_them do
let(:token) { user_token ? personal_access_token.token : 'wrong' }
let(:headers) { user_role == :anonymous ? {} : basic_auth_header(user.username, token) }
before do
project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(visibility_level))
project.send("add_#{user_role}", user) if member && user_role != :anonymous
end
it_behaves_like params[:shared_examples_name], params[:expected_status]
end
end
end
it_behaves_like 'a deploy token for RPM requests'
it_behaves_like 'a job token for RPM requests'
end
describe 'POST /api/v4/projects/:project_id/packages/rpm/authorize' do
let(:url) { api("/projects/#{project.id}/packages/rpm/authorize") }
subject { post(url, headers: headers) }
it_behaves_like 'returning response status', :not_found it_behaves_like 'returning response status', :not_found
context 'when feature flag is disabled' do context 'when feature flag is disabled' do
@ -27,36 +208,4 @@ RSpec.describe API::RpmProjectPackages do
it_behaves_like 'returning response status', :not_found it_behaves_like 'returning response status', :not_found
end end
end end
describe 'GET /api/v4/projects/:project_id/packages/rpm/repodata/:filename' do
let(:url) { api("/projects/#{project.id}/packages/rpm/repodata/#{package_name}") }
subject { get(url, headers: headers) }
it_behaves_like 'an unimplemented route'
end
describe 'GET /api/v4/projects/:project_id/packages/rpm/:package_file_id/:filename' do
let(:url) { api("/projects/#{project.id}/packages/rpm/#{package_file_id}/#{package_name}") }
subject { get(url, headers: headers) }
it_behaves_like 'an unimplemented route'
end
describe 'POST /api/v4/projects/:project_id/packages/rpm/authorize' do
let(:url) { api("/projects/#{project.id}/packages/rpm/authorize") }
subject { post(url, headers: headers) }
it_behaves_like 'an unimplemented route'
end
describe 'POST /api/v4/projects/:project_id/packages/rpm' do
let(:url) { api("/projects/#{project.id}/packages/rpm") }
subject { post(url, headers: headers) }
it_behaves_like 'an unimplemented route'
end
end end

View File

@ -98,30 +98,12 @@ RSpec.describe 'Merge Requests Context Commit Diffs' do
end end
context 'when using ETags' do context 'when using ETags' do
context 'when etag_merge_request_diff_batches is true' do it 'does not serialize diffs' do
it 'does not serialize diffs' do expect(PaginatedDiffSerializer).not_to receive(:new)
expect(PaginatedDiffSerializer).not_to receive(:new)
go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5) go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5)
expect(response).to have_gitlab_http_status(:not_modified) expect(response).to have_gitlab_http_status(:not_modified)
end
end
context 'when etag_merge_request_diff_batches is false' do
before do
stub_feature_flags(etag_merge_request_diff_batches: false)
end
it 'does not serialize diffs' do
expect_next_instance_of(PaginatedDiffSerializer) do |instance|
expect(instance).not_to receive(:represent)
end
go(headers: { 'If-None-Match' => response.etag }, page: 0, per_page: 5)
expect(response).to have_gitlab_http_status(:success)
end
end end
end end

View File

@ -97,34 +97,14 @@ RSpec.describe 'Merge Requests Diffs' do
end end
context 'when using ETags' do context 'when using ETags' do
context 'when etag_merge_request_diff_batches is true' do let(:headers) { { 'If-None-Match' => response.etag } }
let(:headers) { { 'If-None-Match' => response.etag } }
it 'does not serialize diffs' do it 'does not serialize diffs' do
expect(PaginatedDiffSerializer).not_to receive(:new) expect(PaginatedDiffSerializer).not_to receive(:new)
go(headers: headers, page: 0, per_page: 5) go(headers: headers, page: 0, per_page: 5)
expect(response).to have_gitlab_http_status(:not_modified) expect(response).to have_gitlab_http_status(:not_modified)
end
end
context 'when etag_merge_request_diff_batches is false' do
let(:headers) { { 'If-None-Match' => response.etag } }
before do
stub_feature_flags(etag_merge_request_diff_batches: false)
end
it 'does not serialize diffs' do
expect_next_instance_of(PaginatedDiffSerializer) do |instance|
expect(instance).not_to receive(:represent)
end
subject
expect(response).to have_gitlab_http_status(:success)
end
end end
end end
@ -279,28 +259,12 @@ RSpec.describe 'Merge Requests Diffs' do
context 'when using ETag caching' do context 'when using ETag caching' do
let(:headers) { { 'If-None-Match' => response.etag } } let(:headers) { { 'If-None-Match' => response.etag } }
context 'when etag_merge_request_diff_batches is true' do it 'does not serialize diffs' do
it 'does not serialize diffs' do expect(PaginatedDiffSerializer).not_to receive(:new)
expect(PaginatedDiffSerializer).not_to receive(:new)
subject subject
expect(response).to have_gitlab_http_status(:not_modified) expect(response).to have_gitlab_http_status(:not_modified)
end
end
context 'when etag_merge_request_diff_batches is false' do
before do
stub_feature_flags(etag_merge_request_diff_batches: false)
end
it 'does not use cache' do
expect(Rails.cache).not_to receive(:fetch).with(/cache:gitlab:PaginatedDiffSerializer/).and_call_original
subject
expect(response).to have_gitlab_http_status(:success)
end
end end
end end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::Queue::PendingBuildsStrategy do
let_it_be(:group) { create(:group) }
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group]) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
let!(:build_1) { create(:ci_build, :created, pipeline: pipeline) }
let!(:build_2) { create(:ci_build, :created, pipeline: pipeline) }
let!(:build_3) { create(:ci_build, :created, pipeline: pipeline) }
let!(:pending_build_1) { create(:ci_pending_build, build: build_2, project: project) }
let!(:pending_build_2) { create(:ci_pending_build, build: build_3, project: project) }
let!(:pending_build_3) { create(:ci_pending_build, build: build_1, project: project) }
describe 'builds_for_group_runner' do
it 'returns builds ordered by build ID' do
strategy = described_class.new(group_runner)
expect(strategy.builds_for_group_runner).to eq([pending_build_3, pending_build_1, pending_build_2])
end
end
end

View File

@ -34,7 +34,7 @@ require (
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 golang.org/x/lint v0.0.0-20210508222113-6edffad5e616
golang.org/x/net v0.0.0-20220531201128-c960675eff93 golang.org/x/net v0.0.0-20220531201128-c960675eff93
golang.org/x/tools v0.1.11 golang.org/x/tools v0.1.11
google.golang.org/grpc v1.48.0 google.golang.org/grpc v1.49.0
google.golang.org/protobuf v1.28.1 google.golang.org/protobuf v1.28.1
honnef.co/go/tools v0.3.3 honnef.co/go/tools v0.3.3
) )

View File

@ -1780,8 +1780,9 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=