Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1361891b0a
commit
e91cb68359
|
@ -22,7 +22,7 @@
|
|||
RUBY_GC_MALLOC_LIMIT_MAX: 134217728
|
||||
CRYSTALBALL: "true"
|
||||
RECORD_DEPRECATIONS: "true"
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets"]
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets", "detect-tests"]
|
||||
script:
|
||||
- *base-script
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag ~level:migration"
|
||||
|
@ -64,7 +64,7 @@
|
|||
- .rspec-base
|
||||
- .as-if-foss
|
||||
- .use-pg11
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets as-if-foss"]
|
||||
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets as-if-foss", "detect-tests"]
|
||||
|
||||
.rspec-ee-base-pg11:
|
||||
extends:
|
||||
|
|
|
@ -604,10 +604,9 @@
|
|||
rules:
|
||||
- <<: *if-not-ee
|
||||
when: never
|
||||
- <<: *if-security-merge-request
|
||||
changes: *code-backstage-patterns
|
||||
- <<: *if-dot-com-gitlab-org-merge-request
|
||||
- <<: *if-default-refs
|
||||
changes: *code-backstage-patterns
|
||||
- <<: *if-merge-request-title-run-all-rspec
|
||||
|
||||
.rails:rules:rspec-foss-impact:
|
||||
rules:
|
||||
|
|
|
@ -61,15 +61,17 @@ verify-tests-yml:
|
|||
- scripts/verify-tff-mapping
|
||||
|
||||
.detect-test-base:
|
||||
image: ruby:2.7-alpine
|
||||
image: ruby:2.7
|
||||
needs: []
|
||||
stage: prepare
|
||||
script:
|
||||
- source scripts/utils.sh
|
||||
- source ./scripts/utils.sh
|
||||
- source ./scripts/rspec_helpers.sh
|
||||
- install_gitlab_gem
|
||||
- install_tff_gem
|
||||
- tooling/bin/find_foss_tests ${MATCHED_TESTS_FILE}
|
||||
- 'echo "test files affected: $(cat $MATCHED_TESTS_FILE)"'
|
||||
- retrieve_tests_mapping
|
||||
- 'if [ -n "$CI_MERGE_REQUEST_IID" ]; then tooling/bin/find_tests ${MATCHED_TESTS_FILE}; fi'
|
||||
- 'if [ -n "$CI_MERGE_REQUEST_IID" ]; then echo "test files affected: $(cat $MATCHED_TESTS_FILE)"; fi'
|
||||
artifacts:
|
||||
expire_in: 7d
|
||||
paths:
|
||||
|
|
|
@ -1 +1 @@
|
|||
0bdcff0f59fb1bc52eb93e930e53965b19296c99
|
||||
c0ea152ccad891cda5fd255c1fea78562aae5e4a
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
<script>
|
||||
/* eslint-disable @gitlab/vue-require-i18n-strings */
|
||||
import { isEmpty } from 'lodash';
|
||||
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { GlTooltipDirective, GlIcon, GlLink } from '@gitlab/ui';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
|
||||
import CommitComponent from '~/vue_shared/components/commit.vue';
|
||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
|
||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
import eventHub from '../event_hub';
|
||||
import ActionsComponent from './environment_actions.vue';
|
||||
import ExternalUrlComponent from './environment_external_url.vue';
|
||||
|
@ -30,6 +31,7 @@ export default {
|
|||
CommitComponent,
|
||||
ExternalUrlComponent,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
MonitoringButtonComponent,
|
||||
PinComponent,
|
||||
DeleteComponent,
|
||||
|
@ -38,6 +40,7 @@ export default {
|
|||
TerminalButtonComponent,
|
||||
TooltipOnTruncate,
|
||||
UserAvatarLink,
|
||||
CiIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -80,6 +83,24 @@ export default {
|
|||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Object|Undefined} The `upcoming_deployment` object if it exists.
|
||||
* Otherwise, `undefined`.
|
||||
*/
|
||||
upcomingDeployment() {
|
||||
return this.model?.upcoming_deployment;
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {String} Text that will be shown in the tooltip when
|
||||
* the user hovers over the upcoming deployment's status icon.
|
||||
*/
|
||||
upcomingDeploymentTooltipText() {
|
||||
return sprintf(s__('Environments|Deployment %{status}'), {
|
||||
status: this.upcomingDeployment.deployable.status.text,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Checkes whether the row displayed is a folder.
|
||||
*
|
||||
|
@ -234,6 +255,18 @@ export default {
|
|||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Same as `userImageAltDescription`, but for the
|
||||
* upcoming deployment's user
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
upcomingDeploymentUserImageAltDescription() {
|
||||
return sprintf(__("%{username}'s avatar"), {
|
||||
username: this.upcomingDeployment.user.username,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided, returns the commit tag.
|
||||
*
|
||||
|
@ -381,6 +414,15 @@ export default {
|
|||
return '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Same as `deploymentInternalId`, but for the upcoming deployment
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
upcomingDeploymentInternalId() {
|
||||
return `#${this.upcomingDeployment.iid}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verifies if the user object is present under last_deployment object.
|
||||
*
|
||||
|
@ -503,6 +545,13 @@ export default {
|
|||
folderIconName() {
|
||||
return this.model.isOpen ? 'chevron-down' : 'chevron-right';
|
||||
},
|
||||
|
||||
upcomingDeploymentCellClasses() {
|
||||
return [
|
||||
this.tableData.upcoming.spacing,
|
||||
{ 'gl-display-none gl-display-md-block': !this.upcomingDeployment },
|
||||
];
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -512,6 +561,19 @@ export default {
|
|||
onClickFolder() {
|
||||
eventHub.$emit('toggleFolder', this.model);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the field title that will be shown in the field's row
|
||||
* in the mobile view.
|
||||
*
|
||||
* @returns `field.mobileTitle` if present;
|
||||
* if not, falls back to `field.title`.
|
||||
*/
|
||||
getMobileViewTitleForField(fieldName) {
|
||||
const field = this.tableData[fieldName];
|
||||
|
||||
return field.mobileTitle || field.title;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -530,7 +592,7 @@ export default {
|
|||
role="gridcell"
|
||||
>
|
||||
<div v-if="!isFolder" class="table-mobile-header" role="rowheader">
|
||||
{{ tableData.name.title }}
|
||||
{{ getMobileViewTitleForField('name') }}
|
||||
</div>
|
||||
|
||||
<span v-if="shouldRenderDeployBoard" class="deploy-board-icon" @click="toggleDeployBoard">
|
||||
|
@ -609,7 +671,9 @@ export default {
|
|||
</div>
|
||||
|
||||
<div v-if="!isFolder" class="table-section" :class="tableData.commit.spacing" role="gridcell">
|
||||
<div role="rowheader" class="table-mobile-header">{{ tableData.commit.title }}</div>
|
||||
<div role="rowheader" class="table-mobile-header">
|
||||
{{ getMobileViewTitleForField('commit') }}
|
||||
</div>
|
||||
<div v-if="hasLastDeploymentKey" class="js-commit-component table-mobile-content">
|
||||
<commit-component
|
||||
:tag="commitTag"
|
||||
|
@ -623,7 +687,9 @@ export default {
|
|||
</div>
|
||||
|
||||
<div v-if="!isFolder" class="table-section" :class="tableData.date.spacing" role="gridcell">
|
||||
<div role="rowheader" class="table-mobile-header">{{ tableData.date.title }}</div>
|
||||
<div role="rowheader" class="table-mobile-header">
|
||||
{{ getMobileViewTitleForField('date') }}
|
||||
</div>
|
||||
<span
|
||||
v-if="canShowDeploymentDate"
|
||||
v-gl-tooltip
|
||||
|
@ -636,8 +702,51 @@ export default {
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isFolder"
|
||||
class="table-section"
|
||||
:class="upcomingDeploymentCellClasses"
|
||||
role="gridcell"
|
||||
data-testid="upcoming-deployment"
|
||||
>
|
||||
<div role="rowheader" class="table-mobile-header">
|
||||
{{ getMobileViewTitleForField('upcoming') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="upcomingDeployment"
|
||||
class="gl-w-full gl-display-flex gl-flex-direction-row gl-md-flex-direction-column! gl-justify-content-end"
|
||||
data-testid="upcoming-deployment-content"
|
||||
>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<span class="gl-mr-2">{{ upcomingDeploymentInternalId }}</span>
|
||||
<gl-link
|
||||
v-if="upcomingDeployment.deployable"
|
||||
v-gl-tooltip
|
||||
:href="upcomingDeployment.deployable.build_path"
|
||||
:title="upcomingDeploymentTooltipText"
|
||||
data-testid="upcoming-deployment-status-link"
|
||||
>
|
||||
<ci-icon class="gl-mr-2" :status="upcomingDeployment.deployable.status" />
|
||||
</gl-link>
|
||||
</div>
|
||||
<div class="gl-display-flex">
|
||||
<span v-if="upcomingDeployment.user" class="text-break-word">
|
||||
by
|
||||
<user-avatar-link
|
||||
:link-href="upcomingDeployment.user.web_url"
|
||||
:img-src="upcomingDeployment.user.avatar_url"
|
||||
:img-alt="upcomingDeploymentUserImageAltDescription"
|
||||
:tooltip-text="upcomingDeployment.user.username"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isFolder" class="table-section" :class="tableData.autoStop.spacing" role="gridcell">
|
||||
<div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div>
|
||||
<div role="rowheader" class="table-mobile-header">
|
||||
{{ getMobileViewTitleForField('autoStop') }}
|
||||
</div>
|
||||
<span
|
||||
v-if="canShowAutoStopDate"
|
||||
v-gl-tooltip
|
||||
|
|
|
@ -71,7 +71,7 @@ export default {
|
|||
// percent spacing for cols, should add up to 100
|
||||
name: {
|
||||
title: s__('Environments|Environment'),
|
||||
spacing: 'section-15',
|
||||
spacing: 'section-10',
|
||||
},
|
||||
deploy: {
|
||||
title: s__('Environments|Deployment'),
|
||||
|
@ -83,18 +83,23 @@ export default {
|
|||
},
|
||||
commit: {
|
||||
title: s__('Environments|Commit'),
|
||||
spacing: 'section-20',
|
||||
spacing: 'section-15',
|
||||
},
|
||||
date: {
|
||||
title: s__('Environments|Updated'),
|
||||
spacing: 'section-10',
|
||||
},
|
||||
upcoming: {
|
||||
title: s__('Environments|Upcoming'),
|
||||
mobileTitle: s__('Environments|Upcoming deployment'),
|
||||
spacing: 'section-10',
|
||||
},
|
||||
autoStop: {
|
||||
title: s__('Environments|Auto stop in'),
|
||||
spacing: 'section-5',
|
||||
spacing: 'section-10',
|
||||
},
|
||||
actions: {
|
||||
spacing: 'section-25',
|
||||
spacing: 'section-20',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
@ -160,6 +165,9 @@ export default {
|
|||
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
|
||||
{{ tableData.date.title }}
|
||||
</div>
|
||||
<div class="table-section" :class="tableData.upcoming.spacing" role="columnheader">
|
||||
{{ tableData.upcoming.title }}
|
||||
</div>
|
||||
<div class="table-section" :class="tableData.autoStop.spacing" role="columnheader">
|
||||
{{ tableData.autoStop.title }}
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
import { escape, capitalize } from 'lodash';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import StageColumnComponentLegacy from './stage_column_component_legacy.vue';
|
||||
import GraphWidthMixin from '../../mixins/graph_width_mixin';
|
||||
import LinkedPipelinesColumnLegacy from './linked_pipelines_column_legacy.vue';
|
||||
import GraphBundleMixin from '../../mixins/graph_pipeline_bundle_mixin';
|
||||
import { UPSTREAM, DOWNSTREAM, MAIN } from './constants';
|
||||
|
@ -14,7 +13,7 @@ export default {
|
|||
LinkedPipelinesColumnLegacy,
|
||||
StageColumnComponentLegacy,
|
||||
},
|
||||
mixins: [GraphWidthMixin, GraphBundleMixin],
|
||||
mixins: [GraphBundleMixin],
|
||||
props: {
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
|
@ -183,87 +182,83 @@ export default {
|
|||
class="pipeline-visualization pipeline-graph"
|
||||
:class="{ 'pipeline-tab-content': !isLinkedPipeline }"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
paddingLeft: `${graphLeftPadding}px`,
|
||||
paddingRight: `${graphRightPadding}px`,
|
||||
}"
|
||||
>
|
||||
<gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
|
||||
|
||||
<pipeline-graph-legacy
|
||||
v-if="pipelineTypeUpstream"
|
||||
:type="$options.upstream"
|
||||
class="d-inline-block upstream-pipeline"
|
||||
:class="`js-upstream-pipeline-${expandedUpstream.id}`"
|
||||
:is-loading="false"
|
||||
:pipeline="expandedUpstream"
|
||||
:is-linked-pipeline="true"
|
||||
:mediator="mediator"
|
||||
@onClickUpstreamPipeline="clickUpstreamPipeline"
|
||||
@refreshPipelineGraph="requestRefreshPipelineGraph"
|
||||
/>
|
||||
|
||||
<linked-pipelines-column-legacy
|
||||
v-if="hasUpstream"
|
||||
:type="$options.upstream"
|
||||
:linked-pipelines="upstreamPipelines"
|
||||
:column-title="__('Upstream')"
|
||||
:project-id="pipelineProjectId"
|
||||
@linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)"
|
||||
/>
|
||||
|
||||
<ul
|
||||
v-if="!isLoading"
|
||||
:class="{
|
||||
'inline js-has-linked-pipelines': hasDownstream || hasUpstream,
|
||||
}"
|
||||
class="stage-column-list align-top"
|
||||
>
|
||||
<stage-column-component-legacy
|
||||
v-for="(stage, index) in graph"
|
||||
:key="stage.name"
|
||||
:class="{
|
||||
'has-upstream gl-ml-11': hasUpstreamColumn(index),
|
||||
'has-only-one-job': hasOnlyOneJob(stage),
|
||||
'gl-mr-26': shouldAddRightMargin(index),
|
||||
}"
|
||||
:title="capitalizeStageName(stage.name)"
|
||||
:groups="stage.groups"
|
||||
:stage-connector-class="stageConnectorClass(index, stage)"
|
||||
:is-first-column="isFirstColumn(index)"
|
||||
:has-upstream="hasUpstream"
|
||||
:action="stage.status.action"
|
||||
:job-hovered="jobName"
|
||||
:pipeline-expanded="pipelineExpanded"
|
||||
@refreshPipelineGraph="refreshPipelineGraph"
|
||||
<div class="gl-w-full">
|
||||
<div class="container-fluid container-limited">
|
||||
<gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
|
||||
<pipeline-graph-legacy
|
||||
v-if="pipelineTypeUpstream"
|
||||
:type="$options.upstream"
|
||||
class="d-inline-block upstream-pipeline"
|
||||
:class="`js-upstream-pipeline-${expandedUpstream.id}`"
|
||||
:is-loading="false"
|
||||
:pipeline="expandedUpstream"
|
||||
:is-linked-pipeline="true"
|
||||
:mediator="mediator"
|
||||
@onClickUpstreamPipeline="clickUpstreamPipeline"
|
||||
@refreshPipelineGraph="requestRefreshPipelineGraph"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<linked-pipelines-column-legacy
|
||||
v-if="hasDownstream"
|
||||
:type="$options.downstream"
|
||||
:linked-pipelines="downstreamPipelines"
|
||||
:column-title="__('Downstream')"
|
||||
:project-id="pipelineProjectId"
|
||||
@linkedPipelineClick="handleClickedDownstream"
|
||||
@downstreamHovered="setJob"
|
||||
@pipelineExpandToggle="setPipelineExpanded"
|
||||
/>
|
||||
<linked-pipelines-column-legacy
|
||||
v-if="hasUpstream"
|
||||
:type="$options.upstream"
|
||||
:linked-pipelines="upstreamPipelines"
|
||||
:column-title="__('Upstream')"
|
||||
:project-id="pipelineProjectId"
|
||||
@linkedPipelineClick="$emit('onClickUpstreamPipeline', $event)"
|
||||
/>
|
||||
|
||||
<pipeline-graph-legacy
|
||||
v-if="pipelineTypeDownstream"
|
||||
:type="$options.downstream"
|
||||
class="d-inline-block"
|
||||
:class="`js-downstream-pipeline-${expandedDownstream.id}`"
|
||||
:is-loading="false"
|
||||
:pipeline="expandedDownstream"
|
||||
:is-linked-pipeline="true"
|
||||
:style="{ 'margin-top': downstreamMarginTop }"
|
||||
:mediator="mediator"
|
||||
@onClickDownstreamPipeline="clickDownstreamPipeline"
|
||||
@refreshPipelineGraph="requestRefreshPipelineGraph"
|
||||
/>
|
||||
<ul
|
||||
v-if="!isLoading"
|
||||
:class="{
|
||||
'inline js-has-linked-pipelines': hasDownstream || hasUpstream,
|
||||
}"
|
||||
class="stage-column-list align-top"
|
||||
>
|
||||
<stage-column-component-legacy
|
||||
v-for="(stage, index) in graph"
|
||||
:key="stage.name"
|
||||
:class="{
|
||||
'has-upstream gl-ml-11': hasUpstreamColumn(index),
|
||||
'has-only-one-job': hasOnlyOneJob(stage),
|
||||
'gl-mr-26': shouldAddRightMargin(index),
|
||||
}"
|
||||
:title="capitalizeStageName(stage.name)"
|
||||
:groups="stage.groups"
|
||||
:stage-connector-class="stageConnectorClass(index, stage)"
|
||||
:is-first-column="isFirstColumn(index)"
|
||||
:has-upstream="hasUpstream"
|
||||
:action="stage.status.action"
|
||||
:job-hovered="jobName"
|
||||
:pipeline-expanded="pipelineExpanded"
|
||||
@refreshPipelineGraph="refreshPipelineGraph"
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<linked-pipelines-column-legacy
|
||||
v-if="hasDownstream"
|
||||
:type="$options.downstream"
|
||||
:linked-pipelines="downstreamPipelines"
|
||||
:column-title="__('Downstream')"
|
||||
:project-id="pipelineProjectId"
|
||||
@linkedPipelineClick="handleClickedDownstream"
|
||||
@downstreamHovered="setJob"
|
||||
@pipelineExpandToggle="setPipelineExpanded"
|
||||
/>
|
||||
|
||||
<pipeline-graph-legacy
|
||||
v-if="pipelineTypeDownstream"
|
||||
:type="$options.downstream"
|
||||
class="d-inline-block"
|
||||
:class="`js-downstream-pipeline-${expandedDownstream.id}`"
|
||||
:is-loading="false"
|
||||
:pipeline="expandedDownstream"
|
||||
:is-linked-pipeline="true"
|
||||
:style="{ 'margin-top': downstreamMarginTop }"
|
||||
:mediator="mediator"
|
||||
@onClickDownstreamPipeline="clickDownstreamPipeline"
|
||||
@refreshPipelineGraph="requestRefreshPipelineGraph"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
import { debounceByAnimationFrame } from '~/lib/utils/common_utils';
|
||||
import { LAYOUT_CHANGE_DELAY } from '~/pipelines/constants';
|
||||
|
||||
export default {
|
||||
debouncedResize: null,
|
||||
sidebarMutationObserver: null,
|
||||
data() {
|
||||
return {
|
||||
graphLeftPadding: 0,
|
||||
graphRightPadding: 0,
|
||||
};
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.$options.debouncedResize);
|
||||
|
||||
if (this.$options.sidebarMutationObserver) {
|
||||
this.$options.sidebarMutationObserver.disconnect();
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$options.debouncedResize = debounceByAnimationFrame(this.setGraphPadding);
|
||||
window.addEventListener('resize', this.$options.debouncedResize);
|
||||
},
|
||||
mounted() {
|
||||
this.setGraphPadding();
|
||||
|
||||
this.$options.sidebarMutationObserver = new MutationObserver(this.handleLayoutChange);
|
||||
this.$options.sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
|
||||
attributes: true,
|
||||
childList: false,
|
||||
subtree: false,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
setGraphPadding() {
|
||||
// only add padding to main graph (not inline upstream/downstream graphs)
|
||||
if (this.type && this.type !== 'main') return;
|
||||
|
||||
const container = document.querySelector('.js-pipeline-container');
|
||||
if (!container) return;
|
||||
|
||||
this.graphLeftPadding = container.offsetLeft;
|
||||
this.graphRightPadding = window.innerWidth - container.offsetLeft - container.offsetWidth;
|
||||
},
|
||||
handleLayoutChange() {
|
||||
// wait until animations finish, then recalculate padding
|
||||
window.setTimeout(this.setGraphPadding, LAYOUT_CHANGE_DELAY);
|
||||
},
|
||||
},
|
||||
};
|
|
@ -60,6 +60,7 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
formattedMarkdown: null,
|
||||
parsedSource: parseSourceFile(this.preProcess(true, this.content)),
|
||||
editorMode: EDITOR_TYPES.wysiwyg,
|
||||
hasMatter: false,
|
||||
|
@ -140,10 +141,14 @@ export default {
|
|||
onSubmit() {
|
||||
const preProcessedContent = this.preProcess(false, this.parsedSource.content());
|
||||
this.$emit('submit', {
|
||||
formattedMarkdown: this.formattedMarkdown,
|
||||
content: preProcessedContent,
|
||||
images: this.$options.imageRepository.getAll(),
|
||||
});
|
||||
},
|
||||
onEditorLoad({ formattedMarkdown }) {
|
||||
this.formattedMarkdown = formattedMarkdown;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -167,6 +172,7 @@ export default {
|
|||
@modeChange="onModeChange"
|
||||
@input="onInputChange"
|
||||
@uploadImage="onUploadImage"
|
||||
@load="onEditorLoad"
|
||||
/>
|
||||
<unsaved-changes-confirm-dialog :modified="isSaveable" />
|
||||
<publish-toolbar
|
||||
|
|
|
@ -15,6 +15,14 @@ export const LOAD_CONTENT_ERROR = __(
|
|||
'An error ocurred while loading your content. Please try again.',
|
||||
);
|
||||
|
||||
export const DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE = s__(
|
||||
'StaticSiteEditor|Automatic formatting changes',
|
||||
);
|
||||
|
||||
export const DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION = s__(
|
||||
'StaticSiteEditor|Markdown formatting preferences introduced by the Static Site Editor',
|
||||
);
|
||||
|
||||
export const DEFAULT_HEADING = s__('StaticSiteEditor|Static site editor');
|
||||
|
||||
export const TRACKING_ACTION_CREATE_COMMIT = 'create_commit';
|
||||
|
|
|
@ -4,7 +4,17 @@ import savedContentMetaQuery from '../queries/saved_content_meta.query.graphql';
|
|||
|
||||
const submitContentChangesResolver = (
|
||||
_,
|
||||
{ input: { project: projectId, username, sourcePath, content, images, mergeRequestMeta } },
|
||||
{
|
||||
input: {
|
||||
project: projectId,
|
||||
username,
|
||||
sourcePath,
|
||||
content,
|
||||
images,
|
||||
mergeRequestMeta,
|
||||
formattedMarkdown,
|
||||
},
|
||||
},
|
||||
{ cache },
|
||||
) => {
|
||||
return submitContentChanges({
|
||||
|
@ -14,6 +24,7 @@ const submitContentChangesResolver = (
|
|||
content,
|
||||
images,
|
||||
mergeRequestMeta,
|
||||
formattedMarkdown,
|
||||
}).then(savedContentMeta => {
|
||||
const data = produce(savedContentMeta, draftState => {
|
||||
return {
|
||||
|
|
|
@ -53,6 +53,7 @@ export default {
|
|||
return {
|
||||
content: null,
|
||||
images: null,
|
||||
formattedMarkdown: null,
|
||||
submitChangesError: null,
|
||||
isSavingChanges: false,
|
||||
};
|
||||
|
@ -79,9 +80,10 @@ export default {
|
|||
onDismissError() {
|
||||
this.submitChangesError = null;
|
||||
},
|
||||
onPrepareSubmit({ content, images }) {
|
||||
onPrepareSubmit({ formattedMarkdown, content, images }) {
|
||||
this.content = content;
|
||||
this.images = images;
|
||||
this.formattedMarkdown = formattedMarkdown;
|
||||
|
||||
this.isSavingChanges = true;
|
||||
this.$refs.editMetaModal.show();
|
||||
|
@ -110,6 +112,7 @@ export default {
|
|||
username: this.appData.username,
|
||||
sourcePath: this.appData.sourcePath,
|
||||
content: this.content,
|
||||
formattedMarkdown: this.formattedMarkdown,
|
||||
images: this.images,
|
||||
mergeRequestMeta,
|
||||
},
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
TRACKING_ACTION_CREATE_MERGE_REQUEST,
|
||||
USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
|
||||
USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
|
||||
DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE,
|
||||
DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
|
||||
} from '../constants';
|
||||
|
||||
const createBranch = (projectId, branch) =>
|
||||
|
@ -47,7 +49,15 @@ const createImageActions = (images, markdown) => {
|
|||
return actions;
|
||||
};
|
||||
|
||||
const commitContent = (projectId, message, branch, sourcePath, content, images) => {
|
||||
const createUpdateSourceFileAction = (sourcePath, content) => [
|
||||
convertObjectPropsToSnakeCase({
|
||||
action: 'update',
|
||||
filePath: sourcePath,
|
||||
content,
|
||||
}),
|
||||
];
|
||||
|
||||
const commit = (projectId, message, branch, actions) => {
|
||||
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_COMMIT);
|
||||
Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_COMMIT);
|
||||
|
||||
|
@ -56,14 +66,7 @@ const commitContent = (projectId, message, branch, sourcePath, content, images)
|
|||
convertObjectPropsToSnakeCase({
|
||||
branch,
|
||||
commitMessage: message,
|
||||
actions: [
|
||||
convertObjectPropsToSnakeCase({
|
||||
action: 'update',
|
||||
filePath: sourcePath,
|
||||
content,
|
||||
}),
|
||||
...createImageActions(images, content),
|
||||
],
|
||||
actions,
|
||||
}),
|
||||
).catch(() => {
|
||||
throw new Error(SUBMIT_CHANGES_COMMIT_ERROR);
|
||||
|
@ -100,6 +103,7 @@ const submitContentChanges = ({
|
|||
content,
|
||||
images,
|
||||
mergeRequestMeta,
|
||||
formattedMarkdown,
|
||||
}) => {
|
||||
const branch = generateBranchName(username);
|
||||
const { title: mergeRequestTitle, description: mergeRequestDescription } = mergeRequestMeta;
|
||||
|
@ -107,10 +111,25 @@ const submitContentChanges = ({
|
|||
|
||||
return createBranch(projectId, branch)
|
||||
.then(({ data: { web_url: url } }) => {
|
||||
const message = `${DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE}\n\n${DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION}`;
|
||||
|
||||
Object.assign(meta, { branch: { label: branch, url } });
|
||||
|
||||
return commitContent(projectId, mergeRequestTitle, branch, sourcePath, content, images);
|
||||
return formattedMarkdown
|
||||
? commit(
|
||||
projectId,
|
||||
message,
|
||||
branch,
|
||||
createUpdateSourceFileAction(sourcePath, formattedMarkdown),
|
||||
)
|
||||
: meta;
|
||||
})
|
||||
.then(() =>
|
||||
commit(projectId, mergeRequestTitle, branch, [
|
||||
...createUpdateSourceFileAction(sourcePath, content),
|
||||
...createImageActions(images, content),
|
||||
]),
|
||||
)
|
||||
.then(({ data: { short_id: label, web_url: url } }) => {
|
||||
Object.assign(meta, { commit: { label, url } });
|
||||
|
||||
|
|
|
@ -469,6 +469,8 @@ export default {
|
|||
:pipeline-id="mr.pipeline.id"
|
||||
:project-id="mr.sourceProjectId"
|
||||
:security-reports-docs-path="mr.securityReportsDocsPath"
|
||||
:target-project-full-path="mr.targetProjectFullPath"
|
||||
:mr-iid="mr.iid"
|
||||
/>
|
||||
|
||||
<grouped-test-reports-app
|
||||
|
|
|
@ -105,6 +105,8 @@ export default {
|
|||
registerHTMLToMarkdownRenderer(editorApi);
|
||||
|
||||
this.addListeners(editorApi);
|
||||
|
||||
this.$emit('load', { formattedMarkdown: editorApi.getMarkdown() });
|
||||
},
|
||||
onOpenAddImageModal() {
|
||||
this.$refs.addImageModal.show();
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
<script>
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'SecurityReportDownloadDropdown',
|
||||
components: {
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
},
|
||||
props: {
|
||||
artifacts: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
artifactText({ name }) {
|
||||
return sprintf(s__('SecurityReports|Download %{artifactName}'), {
|
||||
artifactName: name,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-dropdown
|
||||
:text="s__('SecurityReports|Download results')"
|
||||
:loading="loading"
|
||||
icon="download"
|
||||
right
|
||||
>
|
||||
<gl-dropdown-item
|
||||
v-for="artifact in artifacts"
|
||||
:key="artifact.path"
|
||||
:href="artifact.path"
|
||||
download
|
||||
>
|
||||
{{ artifactText(artifact) }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</template>
|
|
@ -1,3 +1,5 @@
|
|||
import { invert } from 'lodash';
|
||||
|
||||
export const FEEDBACK_TYPE_DISMISSAL = 'dismissal';
|
||||
export const FEEDBACK_TYPE_ISSUE = 'issue';
|
||||
export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request';
|
||||
|
@ -7,3 +9,24 @@ export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request';
|
|||
*/
|
||||
export const REPORT_TYPE_SAST = 'sast';
|
||||
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
|
||||
|
||||
/**
|
||||
* SecurityReportTypeEnum values for use with GraphQL.
|
||||
*
|
||||
* These should correspond to the lowercase security scan report types.
|
||||
*/
|
||||
export const SECURITY_REPORT_TYPE_ENUM_SAST = 'SAST';
|
||||
export const SECURITY_REPORT_TYPE_ENUM_SECRET_DETECTION = 'SECRET_DETECTION';
|
||||
|
||||
/**
|
||||
* A mapping from security scan report types to SecurityReportTypeEnum values.
|
||||
*/
|
||||
export const reportTypeToSecurityReportTypeEnum = {
|
||||
[REPORT_TYPE_SAST]: SECURITY_REPORT_TYPE_ENUM_SAST,
|
||||
[REPORT_TYPE_SECRET_DETECTION]: SECURITY_REPORT_TYPE_ENUM_SECRET_DETECTION,
|
||||
};
|
||||
|
||||
/**
|
||||
* A mapping from SecurityReportTypeEnum values to security scan report types.
|
||||
*/
|
||||
export const securityReportTypeEnumToReportType = invert(reportTypeToSecurityReportTypeEnum);
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
query securityReportDownloadPaths(
|
||||
$projectPath: ID!
|
||||
$iid: String!
|
||||
$reportTypes: [SecurityReportTypeEnum!]
|
||||
) {
|
||||
project(fullPath: $projectPath) {
|
||||
mergeRequest(iid: $iid) {
|
||||
headPipeline {
|
||||
jobs(securityReportTypes: $reportTypes) {
|
||||
nodes {
|
||||
name
|
||||
artifacts {
|
||||
nodes {
|
||||
downloadPath
|
||||
fileType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,10 +8,17 @@ import { s__ } from '~/locale';
|
|||
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
|
||||
import createFlash from '~/flash';
|
||||
import Api from '~/api';
|
||||
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
|
||||
import SecuritySummary from './components/security_summary.vue';
|
||||
import store from './store';
|
||||
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
|
||||
import { REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION } from './constants';
|
||||
import {
|
||||
REPORT_TYPE_SAST,
|
||||
REPORT_TYPE_SECRET_DETECTION,
|
||||
reportTypeToSecurityReportTypeEnum,
|
||||
} from './constants';
|
||||
import securityReportDownloadPathsQuery from './queries/security_report_download_paths.query.graphql';
|
||||
import { extractSecurityReportArtifacts } from './utils';
|
||||
|
||||
export default {
|
||||
store,
|
||||
|
@ -20,6 +27,7 @@ export default {
|
|||
GlLink,
|
||||
GlSprintf,
|
||||
ReportSection,
|
||||
SecurityReportDownloadDropdown,
|
||||
SecuritySummary,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
|
@ -46,6 +54,16 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
targetProjectFullPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
mrIid: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -60,8 +78,44 @@ export default {
|
|||
status: ERROR,
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
reportArtifacts: {
|
||||
query: securityReportDownloadPathsQuery,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.targetProjectFullPath,
|
||||
iid: String(this.mrIid),
|
||||
reportTypes: this.$options.reportTypes.map(
|
||||
reportType => reportTypeToSecurityReportTypeEnum[reportType],
|
||||
),
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.canShowDownloads;
|
||||
},
|
||||
update(data) {
|
||||
return extractSecurityReportArtifacts(this.$options.reportTypes, data);
|
||||
},
|
||||
error(error) {
|
||||
this.showError(error);
|
||||
},
|
||||
result({ loading }) {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Query has completed, so populate the availableSecurityReports.
|
||||
this.onCheckingAvailableSecurityReports(
|
||||
this.reportArtifacts.map(({ reportType }) => reportType),
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['groupedSummaryText', 'summaryStatus']),
|
||||
canShowDownloads() {
|
||||
return this.glFeatures.coreSecurityMrWidgetDownloads;
|
||||
},
|
||||
hasSecurityReports() {
|
||||
return this.availableSecurityReports.length > 0;
|
||||
},
|
||||
|
@ -71,23 +125,26 @@ export default {
|
|||
hasSecretDetectionReports() {
|
||||
return this.availableSecurityReports.includes(REPORT_TYPE_SECRET_DETECTION);
|
||||
},
|
||||
isLoaded() {
|
||||
return this.summaryStatus !== LOADING;
|
||||
isLoadingReportArtifacts() {
|
||||
return this.$apollo.queries.reportArtifacts.loading;
|
||||
},
|
||||
shouldShowDownloadGuidance() {
|
||||
return !this.canShowDownloads && this.summaryStatus !== LOADING;
|
||||
},
|
||||
scansHaveRunMessage() {
|
||||
return this.canShowDownloads
|
||||
? this.$options.i18n.scansHaveRun
|
||||
: this.$options.i18n.scansHaveRunWithDownloadGuidance;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.checkAvailableSecurityReports(this.$options.reportTypes)
|
||||
.then(availableSecurityReports => {
|
||||
this.availableSecurityReports = Array.from(availableSecurityReports);
|
||||
this.fetchCounts();
|
||||
})
|
||||
.catch(error => {
|
||||
createFlash({
|
||||
message: this.$options.i18n.apiError,
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
});
|
||||
if (!this.canShowDownloads) {
|
||||
this.checkAvailableSecurityReports(this.$options.reportTypes)
|
||||
.then(availableSecurityReports => {
|
||||
this.onCheckingAvailableSecurityReports(Array.from(availableSecurityReports));
|
||||
})
|
||||
.catch(this.showError);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(MODULE_SAST, {
|
||||
|
@ -150,13 +207,25 @@ export default {
|
|||
window.mrTabs.tabShown('pipelines');
|
||||
}
|
||||
},
|
||||
onCheckingAvailableSecurityReports(availableSecurityReports) {
|
||||
this.availableSecurityReports = availableSecurityReports;
|
||||
this.fetchCounts();
|
||||
},
|
||||
showError(error) {
|
||||
createFlash({
|
||||
message: this.$options.i18n.apiError,
|
||||
captureError: true,
|
||||
error,
|
||||
});
|
||||
},
|
||||
},
|
||||
reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
|
||||
i18n: {
|
||||
apiError: s__(
|
||||
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
|
||||
),
|
||||
scansHaveRun: s__(
|
||||
scansHaveRun: s__('SecurityReports|Security scans have run'),
|
||||
scansHaveRunWithDownloadGuidance: s__(
|
||||
'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
|
||||
),
|
||||
downloadFromPipelineTab: s__(
|
||||
|
@ -190,7 +259,7 @@ export default {
|
|||
</span>
|
||||
</template>
|
||||
|
||||
<template v-if="isLoaded" #sub-heading>
|
||||
<template v-if="shouldShowDownloadGuidance" #sub-heading>
|
||||
<span class="gl-font-sm">
|
||||
<gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
|
||||
<template #link="{ content }">
|
||||
|
@ -204,6 +273,13 @@ export default {
|
|||
</gl-sprintf>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template v-if="canShowDownloads" #action-buttons>
|
||||
<security-report-download-dropdown
|
||||
:artifacts="reportArtifacts"
|
||||
:loading="isLoadingReportArtifacts"
|
||||
/>
|
||||
</template>
|
||||
</report-section>
|
||||
|
||||
<!-- TODO: Remove this section when removing core_security_mr_widget_counts
|
||||
|
@ -216,7 +292,7 @@ export default {
|
|||
data-testid="security-mr-widget"
|
||||
>
|
||||
<template #error>
|
||||
<gl-sprintf :message="$options.i18n.scansHaveRun">
|
||||
<gl-sprintf :message="scansHaveRunMessage">
|
||||
<template #link="{ content }">
|
||||
<gl-link data-testid="show-pipelines" @click="activatePipelinesTab">{{
|
||||
content
|
||||
|
@ -233,5 +309,12 @@ export default {
|
|||
<gl-icon name="question" />
|
||||
</gl-link>
|
||||
</template>
|
||||
|
||||
<template v-if="canShowDownloads" #action-buttons>
|
||||
<security-report-download-dropdown
|
||||
:artifacts="reportArtifacts"
|
||||
:loading="isLoadingReportArtifacts"
|
||||
/>
|
||||
</template>
|
||||
</report-section>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { securityReportTypeEnumToReportType } from './constants';
|
||||
|
||||
export const extractSecurityReportArtifacts = (reportTypes, data) => {
|
||||
const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? [];
|
||||
|
||||
return jobs.reduce((acc, job) => {
|
||||
const artifacts = job.artifacts?.nodes ?? [];
|
||||
|
||||
artifacts.forEach(({ downloadPath, fileType }) => {
|
||||
const reportType = securityReportTypeEnumToReportType[fileType];
|
||||
if (reportType && reportTypes.includes(reportType)) {
|
||||
acc.push({
|
||||
name: job.name,
|
||||
reportType,
|
||||
path: downloadPath,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
|
@ -129,3 +129,17 @@
|
|||
content: '';
|
||||
display: flex;
|
||||
}
|
||||
|
||||
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1085
|
||||
.gl-md-flex-direction-column {
|
||||
@media (min-width: $breakpoint-md) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
// Same as above
|
||||
.gl-md-flex-direction-column\! {
|
||||
@media (min-width: $breakpoint-md) {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,12 +19,12 @@ module Boards
|
|||
end
|
||||
|
||||
def create
|
||||
list = Boards::Lists::CreateService.new(board.resource_parent, current_user, create_list_params).execute(board)
|
||||
response = Boards::Lists::CreateService.new(board.resource_parent, current_user, create_list_params).execute(board)
|
||||
|
||||
if list.valid?
|
||||
render json: serialize_as_json(list)
|
||||
if response.success?
|
||||
render json: serialize_as_json(response.payload[:list])
|
||||
else
|
||||
render json: list.errors, status: :unprocessable_entity
|
||||
render json: { errors: response.errors }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
|
||||
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
|
||||
push_frontend_feature_flag(:core_security_mr_widget_downloads, @project)
|
||||
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
|
||||
push_frontend_feature_flag(:test_failure_history, @project)
|
||||
push_frontend_feature_flag(:diffs_gradual_load, @project)
|
||||
|
|
|
@ -27,30 +27,16 @@ module Mutations
|
|||
board = authorized_find!(id: args[:board_id])
|
||||
params = create_list_params(args)
|
||||
|
||||
authorize_list_type_resource!(board, params)
|
||||
|
||||
list = create_list(board, params)
|
||||
response = create_list(board, params)
|
||||
|
||||
{
|
||||
list: list.valid? ? list : nil,
|
||||
errors: errors_on_object(list)
|
||||
list: response.success? ? response.payload[:list] : nil,
|
||||
errors: response.errors
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Overridden in EE
|
||||
def authorize_list_type_resource!(board, params)
|
||||
return unless params[:label_id]
|
||||
|
||||
labels = ::Labels::AvailableLabelsService.new(current_user, board.resource_parent, params)
|
||||
.filter_labels_ids_in_param(:label_id)
|
||||
|
||||
unless labels.present?
|
||||
raise Gitlab::Graphql::Errors::ArgumentError, 'Label not found!'
|
||||
end
|
||||
end
|
||||
|
||||
def create_list(board, params)
|
||||
create_list_service =
|
||||
::Boards::Lists::CreateService.new(board.resource_parent, current_user, params)
|
||||
|
|
|
@ -25,8 +25,6 @@ module Enums
|
|||
schedule: 4,
|
||||
api: 5,
|
||||
external: 6,
|
||||
# TODO: Rename `pipeline` to `cross_project_pipeline` in 13.0
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/195991
|
||||
pipeline: 7,
|
||||
chat: 8,
|
||||
webide: 9,
|
||||
|
|
|
@ -7,7 +7,7 @@ class List < ApplicationRecord
|
|||
belongs_to :label
|
||||
has_many :list_user_preferences
|
||||
|
||||
enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3, milestone: 4 }
|
||||
enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3, milestone: 4, iteration: 5 }
|
||||
|
||||
validates :board, :list_type, presence: true, unless: :importing?
|
||||
validates :label, :position, presence: true, if: :label?
|
||||
|
|
|
@ -274,7 +274,7 @@ class MergeRequest < ApplicationRecord
|
|||
scope :with_api_entity_associations, -> {
|
||||
preload_routables
|
||||
.preload(:assignees, :author, :unresolved_notes, :labels, :milestone,
|
||||
:timelogs, :latest_merge_request_diff,
|
||||
:timelogs, :latest_merge_request_diff, :reviewers,
|
||||
target_project: :project_feature,
|
||||
metrics: [:latest_closed_by, :merged_by])
|
||||
}
|
||||
|
|
|
@ -6,17 +6,21 @@ module Boards
|
|||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def execute(board)
|
||||
List.transaction do
|
||||
case type
|
||||
when :backlog
|
||||
create_backlog(board)
|
||||
else
|
||||
target = target(board)
|
||||
position = next_position(board)
|
||||
list = case type
|
||||
when :backlog
|
||||
create_backlog(board)
|
||||
else
|
||||
target = target(board)
|
||||
position = next_position(board)
|
||||
|
||||
create_list(board, type, target, position)
|
||||
end
|
||||
end
|
||||
return ServiceResponse.error(message: _('%{board_target} not found') % { board_target: type.to_s.capitalize }) if target.blank?
|
||||
|
||||
create_list(board, type, target, position)
|
||||
end
|
||||
|
||||
return ServiceResponse.error(message: list.errors.full_messages) unless list.persisted?
|
||||
|
||||
ServiceResponse.success(payload: { list: list })
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -33,7 +37,7 @@ module Boards
|
|||
|
||||
def target(board)
|
||||
strong_memoize(:target) do
|
||||
available_labels.find(params[:label_id])
|
||||
available_labels.find_by(id: params[:label_id]) # rubocop: disable CodeReuse/ActiveRecord
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -7,7 +7,11 @@ module Boards
|
|||
return false unless board.lists.movable.empty?
|
||||
|
||||
List.transaction do
|
||||
label_params.each { |params| create_list(board, params) }
|
||||
label_params.each do |params|
|
||||
response = create_list(board, params)
|
||||
|
||||
raise ActiveRecord::Rollback unless response.success?
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
.branch-commit.cgray
|
||||
- if deployment.ref
|
||||
%span.icon-container.gl-display-inline-block
|
||||
= deployment.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite')
|
||||
= deployment.tag? ? sprite_icon('tag', css_class: 'sprite') : sprite_icon('fork', css_class: 'sprite')
|
||||
= link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name"
|
||||
.icon-container.commit-icon
|
||||
= custom_icon("icon_commit")
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
= render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
|
||||
|
||||
.tab-content
|
||||
#js-tab-pipeline.tab-pane.gl-absolute.gl-left-0.gl-w-full
|
||||
#js-tab-pipeline.tab-pane.gl-w-full
|
||||
#js-pipeline-graph-vue
|
||||
|
||||
#js-tab-builds.tab-pane
|
||||
|
|
|
@ -1610,6 +1610,14 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
:tags: []
|
||||
- :name: gitlab_performance_bar_stats
|
||||
:feature_category: :metrics
|
||||
:has_external_dependencies:
|
||||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 1
|
||||
:idempotent: true
|
||||
:tags: []
|
||||
- :name: gitlab_shell
|
||||
:feature_category: :source_code_management
|
||||
:has_external_dependencies:
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GitlabPerformanceBarStatsWorker
|
||||
include ApplicationWorker
|
||||
|
||||
LEASE_KEY = 'gitlab:performance_bar_stats'
|
||||
LEASE_TIMEOUT = 600
|
||||
WORKER_DELAY = 120
|
||||
STATS_KEY = 'performance_bar_stats:pending_request_ids'
|
||||
|
||||
feature_category :metrics
|
||||
idempotent!
|
||||
|
||||
def perform(lease_uuid)
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
request_ids = fetch_request_ids(redis, lease_uuid)
|
||||
stats = Gitlab::PerformanceBar::Stats.new(redis)
|
||||
|
||||
request_ids.each do |id|
|
||||
stats.process(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_request_ids(redis, lease_uuid)
|
||||
ids = redis.smembers(STATS_KEY)
|
||||
redis.del(STATS_KEY)
|
||||
Gitlab::ExclusiveLease.cancel(LEASE_KEY, lease_uuid)
|
||||
|
||||
ids
|
||||
end
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add iteration_id column to lists
|
||||
merge_request: 48103
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use a separate commit to store formatting changes in the Static Site Editor
|
||||
merge_request: 49052
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Set vulnerability as dismissed when there is dismissal feedback
|
||||
merge_request: 48795
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove references to cross_project_pipeline source in documentation
|
||||
merge_request: 49579
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add upcoming deployment column to Environments page
|
||||
merge_request: 48062
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: core_security_mr_widget_downloads
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48769
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273418
|
||||
milestone: '13.7'
|
||||
type: development
|
||||
group: group::static analysis
|
||||
default_enabled: false
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: performance_bar_stats
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48149
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285480
|
||||
milestone: '13.7'
|
||||
type: development
|
||||
group: group::product_planning
|
||||
default_enabled: false
|
|
@ -142,6 +142,8 @@
|
|||
- 1
|
||||
- - github_importer
|
||||
- 1
|
||||
- - gitlab_performance_bar_stats
|
||||
- 1
|
||||
- - gitlab_shell
|
||||
- 2
|
||||
- - group_destroy
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIterationIdToLists < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :lists, :iteration_id, :bigint
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIterationListsForeignKey < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
INDEX_NAME = 'index_lists_on_iteration_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :lists, :iteration_id, name: INDEX_NAME
|
||||
add_concurrent_foreign_key :lists, :sprints, column: :iteration_id, on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
remove_foreign_key_if_exists :lists, :sprints, column: :iteration_id
|
||||
remove_concurrent_index_by_name :lists, INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,62 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SchedulePopulateDismissedStateForVulnerabilities < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
TMP_INDEX_NAME = 'tmp_index_on_vulnerabilities_non_dismissed'
|
||||
|
||||
DOWNTIME = false
|
||||
BATCH_SIZE = 1_000
|
||||
VULNERABILITY_BATCH_SIZE = 5_000
|
||||
DELAY_INTERVAL = 3.minutes.to_i
|
||||
MIGRATION_CLASS = 'PopulateDismissedStateForVulnerabilities'
|
||||
|
||||
VULNERABILITY_JOIN_CONDITION = 'JOIN "vulnerability_occurrences" ON "vulnerability_occurrences"."vulnerability_id" = "vulnerabilities"."id"'
|
||||
FEEDBACK_WHERE_CONDITION = <<~SQL
|
||||
EXISTS (SELECT 1 FROM vulnerability_feedback
|
||||
WHERE "vulnerability_occurrences"."project_id" = "vulnerability_feedback"."project_id"
|
||||
AND "vulnerability_occurrences"."report_type" = "vulnerability_feedback"."category"
|
||||
AND ENCODE("vulnerability_occurrences"."project_fingerprint", 'hex') = "vulnerability_feedback"."project_fingerprint"
|
||||
AND "vulnerability_feedback"."feedback_type" = 0
|
||||
)
|
||||
SQL
|
||||
|
||||
class Vulnerability < ActiveRecord::Base # rubocop:disable Style/Documentation
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'vulnerabilities'
|
||||
end
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index(:vulnerabilities, :id, where: 'state <> 2', name: TMP_INDEX_NAME)
|
||||
|
||||
batch = []
|
||||
index = 1
|
||||
|
||||
Vulnerability.where('state <> 2').each_batch(of: VULNERABILITY_BATCH_SIZE) do |relation|
|
||||
ids = relation
|
||||
.joins(VULNERABILITY_JOIN_CONDITION)
|
||||
.where(FEEDBACK_WHERE_CONDITION)
|
||||
.pluck('vulnerabilities.id')
|
||||
|
||||
ids.each do |id|
|
||||
batch << id
|
||||
|
||||
if batch.size == BATCH_SIZE
|
||||
migrate_in(index * DELAY_INTERVAL, MIGRATION_CLASS, batch)
|
||||
index += 1
|
||||
|
||||
batch.clear
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
migrate_in(index * DELAY_INTERVAL, MIGRATION_CLASS, batch) unless batch.empty?
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(:vulnerabilities, TMP_INDEX_NAME)
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
6d2e6937c9e41975b1fd402bf2985796792a1e5f8e4f4f98bc76b65ff73c4e02
|
|
@ -0,0 +1 @@
|
|||
c7567489156bbc047cf9f7827f060ad507fd5d328179f2796566a7dc54806e3e
|
|
@ -0,0 +1 @@
|
|||
27cd7e7cd01175c157e6aa666b2263bf29210277d5acd997a0619cee67870345
|
|
@ -13652,7 +13652,8 @@ CREATE TABLE lists (
|
|||
milestone_id integer,
|
||||
max_issue_count integer DEFAULT 0 NOT NULL,
|
||||
max_issue_weight integer DEFAULT 0 NOT NULL,
|
||||
limit_metric character varying(20)
|
||||
limit_metric character varying(20),
|
||||
iteration_id bigint
|
||||
);
|
||||
|
||||
CREATE SEQUENCE lists_id_seq
|
||||
|
@ -21617,6 +21618,8 @@ CREATE UNIQUE INDEX index_list_user_preferences_on_user_id_and_list_id ON list_u
|
|||
|
||||
CREATE UNIQUE INDEX index_lists_on_board_id_and_label_id ON lists USING btree (board_id, label_id);
|
||||
|
||||
CREATE INDEX index_lists_on_iteration_id ON lists USING btree (iteration_id);
|
||||
|
||||
CREATE INDEX index_lists_on_label_id ON lists USING btree (label_id);
|
||||
|
||||
CREATE INDEX index_lists_on_list_type ON lists USING btree (list_type);
|
||||
|
@ -22873,6 +22876,8 @@ CREATE INDEX tmp_build_stage_position_index ON ci_builds USING btree (stage_id,
|
|||
|
||||
CREATE INDEX tmp_index_for_email_unconfirmation_migration ON emails USING btree (id) WHERE (confirmed_at IS NOT NULL);
|
||||
|
||||
CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING btree (id) WHERE (state <> 2);
|
||||
|
||||
CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON merge_request_metrics USING btree (merge_request_id);
|
||||
|
||||
CREATE UNIQUE INDEX vulnerability_feedback_unique_idx ON vulnerability_feedback USING btree (project_id, category, feedback_type, project_fingerprint);
|
||||
|
@ -23263,6 +23268,9 @@ ALTER TABLE ONLY notes
|
|||
ALTER TABLE ONLY members
|
||||
ADD CONSTRAINT fk_2e88fb7ce9 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY lists
|
||||
ADD CONSTRAINT fk_30f2a831f4 FOREIGN KEY (iteration_id) REFERENCES sprints(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY approvals
|
||||
ADD CONSTRAINT fk_310d714958 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -998,6 +998,22 @@ For Omnibus GitLab installations, GitLab Exporter logs reside in `/var/log/gitla
|
|||
For Omnibus GitLab installations, GitLab Kubernetes Agent Server logs reside
|
||||
in `/var/log/gitlab/gitlab-kas/`.
|
||||
|
||||
## Performance bar stats
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48149) in GitLab 13.7.
|
||||
|
||||
This file lives in `/var/log/gitlab/gitlab-rails/performance_bar_json.log` for
|
||||
Omnibus GitLab packages or in `/home/git/gitlab/log/performance_bar_json.log` for
|
||||
installations from source.
|
||||
|
||||
Performance bar statistics (currently only duration of SQL queries) are recorded in that file. For example:
|
||||
|
||||
```json
|
||||
{"severity":"INFO","time":"2020-12-04T09:29:44.592Z","correlation_id":"33680b1490ccd35981b03639c406a697","filename":"app/models/ci/pipeline.rb","filenum":"395","method":"each_with_object","request_id":"rYHomD0VJS4","duration_ms":26.889,"type": "sql"}
|
||||
```
|
||||
|
||||
These statistics are logged on .com only, disabled on self-deployments.
|
||||
|
||||
## Gathering logs
|
||||
|
||||
When [troubleshooting](troubleshooting/index.md) issues that aren't localized to one of the
|
||||
|
|
|
@ -104,7 +104,7 @@ Kubernetes-specific environment variables are detailed in the
|
|||
| `CI_PAGES_URL` | 11.8 | all | URL to GitLab Pages-built pages. Always belongs to a subdomain of `CI_PAGES_DOMAIN`. |
|
||||
| `CI_PIPELINE_ID` | 8.10 | all | The instance-level ID of the current pipeline. This is a unique ID across all projects on GitLab. |
|
||||
| `CI_PIPELINE_IID` | 11.0 | all | The project-level IID (internal ID) of the current pipeline. This ID is unique for the current project. |
|
||||
| `CI_PIPELINE_SOURCE` | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `schedule`, `api`, `external`, `chat`, `webide`, `merge_request_event`, `external_pull_request_event`, `parent_pipeline`, [`trigger`, or `pipeline`](../triggers/README.md#authentication-tokens) (renamed to `cross_project_pipeline` since 13.0). For pipelines created before GitLab 9.5, this is displayed as `unknown`. |
|
||||
| `CI_PIPELINE_SOURCE` | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `schedule`, `api`, `external`, `chat`, `webide`, `merge_request_event`, `external_pull_request_event`, `parent_pipeline`, [`trigger`, or `pipeline`](../triggers/README.md#authentication-tokens). For pipelines created before GitLab 9.5, this is displayed as `unknown`. |
|
||||
| `CI_PIPELINE_TRIGGERED` | all | all | The flag to indicate that job was [triggered](../triggers/README.md) |
|
||||
| `CI_PIPELINE_URL` | 11.1 | 0.5 | Pipeline details URL |
|
||||
| `CI_PROJECT_DIR` | all | all | The full path where the repository is cloned and where the job is run. If the GitLab Runner `builds_dir` parameter is set, this variable is set relative to the value of `builds_dir`. For more information, see [Advanced configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section) for GitLab Runner. |
|
||||
|
|
|
@ -840,11 +840,22 @@ Read more about [configuring NFS mounts](../administration/nfs.md)
|
|||
|
||||
### Restore for installation from source
|
||||
|
||||
First, ensure your backup tar file is in the backup directory described in the
|
||||
`gitlab.yml` configuration:
|
||||
|
||||
```yaml
|
||||
## Backup settings
|
||||
backup:
|
||||
path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/)
|
||||
```
|
||||
|
||||
The default is `/home/git/gitlab/tmp/backups`, and it needs to be owned by the `git` user. Now, you can begin the backup procedure:
|
||||
|
||||
```shell
|
||||
# Stop processes that are connected to the database
|
||||
sudo service gitlab stop
|
||||
|
||||
bundle exec rake gitlab:backup:restore RAILS_ENV=production
|
||||
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
|
||||
```
|
||||
|
||||
Example output:
|
||||
|
|
|
@ -53,10 +53,15 @@ click of a button:
|
|||
|
||||
![Static Site Editor](img/wysiwyg_editor_v13_3.png)
|
||||
|
||||
When an editor submits their changes, in the background, GitLab automatically
|
||||
creates a new branch, commits their changes, and opens a merge request. The
|
||||
editor lands directly on the merge request, and then they can assign it to
|
||||
a colleague for review.
|
||||
When an editor submits their changes, these are the following actions that GitLab
|
||||
performs automatically in the background:
|
||||
|
||||
1. Creates a new branch.
|
||||
1. Commits their changes.
|
||||
1. Fixes formatting according to the [Handbook Markdown Style Guide](https://about.gitlab.com/handbook/markdown-guide/) style guide and add them through another commit.
|
||||
1. Opens a merge request against the default branch.
|
||||
|
||||
The editor can then navigate to the merge request to assign it to a colleague for review.
|
||||
|
||||
## Set up your project
|
||||
|
||||
|
|
|
@ -117,8 +117,6 @@ module API
|
|||
use :list_creation_params
|
||||
end
|
||||
post '/lists' do
|
||||
authorize_list_type_resource!
|
||||
|
||||
authorize!(:admin_list, user_project)
|
||||
|
||||
create_list
|
||||
|
|
|
@ -47,12 +47,12 @@ module API
|
|||
create_list_service =
|
||||
::Boards::Lists::CreateService.new(board_parent, current_user, create_list_params)
|
||||
|
||||
list = create_list_service.execute(board)
|
||||
response = create_list_service.execute(board)
|
||||
|
||||
if list.valid?
|
||||
present list, with: Entities::List
|
||||
if response.success?
|
||||
present response.payload[:list], with: Entities::List
|
||||
else
|
||||
render_validation_error!(list)
|
||||
render_api_error!({ error: response.errors.first }, 400)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -80,14 +80,6 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def authorize_list_type_resource!
|
||||
unless available_labels_for(board_parent).exists?(params[:label_id])
|
||||
render_api_error!({ error: 'Label not found!' }, 400)
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
params :list_creation_params do
|
||||
requires :label_id, type: Integer, desc: 'The ID of an existing label'
|
||||
end
|
||||
|
|
|
@ -27,6 +27,7 @@ module API
|
|||
expose(:downvotes) { |merge_request, options| issuable_metadata.downvotes }
|
||||
|
||||
expose :author, :assignees, :assignee, using: Entities::UserBasic
|
||||
expose :reviewers, if: -> (merge_request, _) { merge_request.allows_reviewers? }, using: Entities::UserBasic
|
||||
expose :source_project_id, :target_project_id
|
||||
expose :labels do |merge_request, options|
|
||||
if options[:with_labels_details]
|
||||
|
|
|
@ -83,8 +83,6 @@ module API
|
|||
use :list_creation_params
|
||||
end
|
||||
post '/lists' do
|
||||
authorize_list_type_resource!
|
||||
|
||||
authorize!(:admin_list, user_group)
|
||||
|
||||
create_list
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# This class updates vulnerabilities entities with state dismissed
|
||||
class PopulateDismissedStateForVulnerabilities
|
||||
class Vulnerability < ActiveRecord::Base # rubocop:disable Style/Documentation
|
||||
self.table_name = 'vulnerabilities'
|
||||
end
|
||||
|
||||
def perform(*vulnerability_ids)
|
||||
Vulnerability.where(id: vulnerability_ids).update_all(state: 2)
|
||||
PopulateMissingVulnerabilityDismissalInformation.new.perform(*vulnerability_ids)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,13 +26,16 @@ module Gitlab
|
|||
|
||||
class Finding < ActiveRecord::Base # rubocop:disable Style/Documentation
|
||||
include ShaAttribute
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
|
||||
self.table_name = 'vulnerability_occurrences'
|
||||
|
||||
sha_attribute :project_fingerprint
|
||||
|
||||
def dismissal_feedback
|
||||
Feedback.dismissal.where(category: report_type, project_fingerprint: project_fingerprint, project_id: project_id).first
|
||||
strong_memoize(:dismissal_feedback) do
|
||||
Feedback.dismissal.where(category: report_type, project_fingerprint: project_fingerprint, project_id: project_id).first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
#
|
||||
# PLEASE DO NOT ADD NEW STRINGS TO THIS FILE.
|
||||
#
|
||||
# See https://docs.gitlab.com/ee/development/i18n/externalization.html#html
|
||||
# for information on how to handle HTML in translations.
|
||||
|
||||
#
|
||||
# This file contains strings that need to be fixed to use the
|
||||
# updated HTML guidelines. Any strings in this file will no
|
||||
# longer be able to be translated until they have been updated.
|
||||
#
|
||||
# This file (and the functionality around it) will be removed
|
||||
# once https://gitlab.com/gitlab-org/gitlab/-/issues/217933 is complete.
|
||||
#
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/19485 for more details
|
||||
# why this change has been made.
|
||||
#
|
||||
|
||||
#
|
||||
# Strings below are fixed in the source code but the translations are still present in CrowdIn so the
|
||||
# locale files will fail the linter. They can be deleted after next CrowdIn sync, likely in:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/226008
|
||||
#
|
||||
|
||||
"This commit was signed with an <strong>unverified</strong> signature.":
|
||||
plural_id:
|
||||
translations:
|
||||
- "このコミットは<strong>検証されていない</strong> 署名でサインされています。"
|
||||
- "Этот коммит был подписан <strong>непроверенной</strong> подписью."
|
||||
- "此提交使用 <strong>未经验证的</strong> 签名进行签名。"
|
||||
- "Цей коміт підписано <strong>неперевіреним</strong> підписом."
|
||||
- "Esta commit fue firmado con una firma <strong>no verificada</strong>."
|
||||
"This commit was signed with a <strong>verified</strong> signature and the committer email is verified to belong to the same user.":
|
||||
plural_id:
|
||||
translations:
|
||||
- "このコミットは <strong>検証済み</strong> の署名でサインされており、このコミッターのメールは同じユーザーのものであることが検証されています。"
|
||||
- "Это коммит был подписан <strong>верифицированной</strong> подписью и коммитер подтвердил, что адрес почты принадлежит ему."
|
||||
- "此提交使用 <strong>已验证</strong> 的签名进行签名,并且已验证提交者的电子邮件属于同一用户。"
|
||||
- "Цей коміт підписано <strong>перевіреним</strong> підписом і адреса електронної пошти комітера гарантовано належить тому самому користувачу."
|
||||
- "Este commit fue firmado con una firma verificada, y <strong>se ha verificado</strong> que la dirección de correo electrónico del committer y la firma pertenecen al mismo usuario."
|
||||
"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}":
|
||||
plural_id:
|
||||
translations:
|
||||
- "分支 <strong>%{branch_name}</strong> 已創建。如需設置自動部署, 請選擇合適的 GitLab CI Yaml 模板併提交更改。%{link_to_autodeploy_doc}"
|
||||
- "O branch <strong>%{branch_name}</strong> foi criado. Para configurar o deploy automático, selecione um modelo de Yaml do GitLab CI e commit suas mudanças. %{link_to_autodeploy_doc}"
|
||||
- "<strong>%{branch_name}</strong> ブランチが作成されました。自動デプロイを設定するには、GitLab CI Yaml テンプレートを選択して、変更をコミットしてください。 %{link_to_autodeploy_doc}"
|
||||
- "La branch <strong>%{branch_name}</strong> è stata creata. Per impostare un rilascio automatico scegli un template CI di Gitlab e committa le tue modifiche %{link_to_autodeploy_doc}"
|
||||
- "O ramo <strong>%{branch_name}</strong> foi criado. Para configurar a implantação automática, seleciona um modelo de Yaml do GitLab CI e envia as tuas alterações. %{link_to_autodeploy_doc}"
|
||||
- "Ветка <strong>%{branch_name}</strong> создана. Для настройки автоматического развертывания выберите YAML-шаблон для GitLab CI и зафиксируйте свои изменения. %{link_to_autodeploy_doc}"
|
||||
- "已创建分支 <strong>%{branch_name}</strong> 。如需设置自动部署, 请选择合适的 GitLab CI Yaml 模板并提交更改。%{link_to_autodeploy_doc}"
|
||||
- "Гілка <strong>%{branch_name}</strong> створена. Для настройки автоматичного розгортання виберіть GitLab CI Yaml-шаблон і закомітьте зміни. %{link_to_autodeploy_doc}"
|
||||
- "Клонът <strong>%{branch_name}</strong> беше създаден. За да настроите автоматичното внедряване, изберете Yaml шаблон за GitLab CI и подайте промените си. %{link_to_autodeploy_doc}"
|
||||
- "Branch <strong>%{branch_name}</strong> wurde erstellt. Um die automatische Bereitstellung einzurichten, wähle eine GitLab CI Yaml Vorlage und committe deine Änderungen. %{link_to_autodeploy_doc}"
|
||||
- "<strong>%{branch_name}</strong> 브랜치가 생성되었습니다. 자동 배포를 설정하려면 GitLab CI Yaml 템플릿을 선택하고 변경 사항을 적용하십시오. %{link_to_autodeploy_doc}"
|
||||
- "La branĉo <strong>%{branch_name}</strong> estis kreita. Por agordi aŭtomatan disponigadon, bonvolu elekti Yaml-ŝablonon por GitLab CI kaj enmeti viajn ŝanĝojn. %{link_to_autodeploy_doc}"
|
||||
- "La branche <strong>%{branch_name}</strong> a été créée. Pour mettre en place le déploiement automatisé, sélectionnez un modèle de fichier YAML pour l’intégration continue (CI) de GitLab, et validez les modifications. %{link_to_autodeploy_doc}"
|
||||
- "La rama <strong>%{branch_name}</strong> fue creada. Para configurar el auto despliegue, escoge una plantilla Yaml para GitLab CI y envía tus cambios. %{link_to_autodeploy_doc}"
|
|
@ -5,14 +5,13 @@ module Gitlab
|
|||
class PoLinter
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
attr_reader :po_path, :translation_entries, :metadata_entry, :locale, :html_todolist
|
||||
attr_reader :po_path, :translation_entries, :metadata_entry, :locale
|
||||
|
||||
VARIABLE_REGEX = /%{\w*}|%[a-z]/.freeze
|
||||
|
||||
def initialize(po_path:, html_todolist:, locale: I18n.locale.to_s)
|
||||
def initialize(po_path:, locale: I18n.locale.to_s)
|
||||
@po_path = po_path
|
||||
@locale = locale
|
||||
@html_todolist = html_todolist
|
||||
end
|
||||
|
||||
def errors
|
||||
|
@ -43,8 +42,7 @@ module Gitlab
|
|||
@translation_entries = entries.map do |entry_data|
|
||||
Gitlab::I18n::TranslationEntry.new(
|
||||
entry_data: entry_data,
|
||||
nplurals: metadata_entry.expected_forms,
|
||||
html_allowed: html_todolist.fetch(entry_data[:msgid], false)
|
||||
nplurals: metadata_entry.expected_forms
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -97,15 +95,15 @@ module Gitlab
|
|||
common_message = 'contains < or >. Use variables to include HTML in the string, or the < and > codes ' \
|
||||
'for the symbols. For more info see: https://docs.gitlab.com/ee/development/i18n/externalization.html#html'
|
||||
|
||||
if entry.msgid_contains_potential_html? && !entry.msgid_html_allowed?
|
||||
if entry.msgid_contains_potential_html?
|
||||
errors << common_message
|
||||
end
|
||||
|
||||
if entry.plural_id_contains_potential_html? && !entry.plural_id_html_allowed?
|
||||
if entry.plural_id_contains_potential_html?
|
||||
errors << 'plural id ' + common_message
|
||||
end
|
||||
|
||||
if entry.translations_contain_potential_html? && !entry.translations_html_allowed?
|
||||
if entry.translations_contain_potential_html?
|
||||
errors << 'translation ' + common_message
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,12 +6,11 @@ module Gitlab
|
|||
PERCENT_REGEX = /(?:^|[^%])%(?!{\w*}|[a-z%])/.freeze
|
||||
ANGLE_BRACKET_REGEX = /[<>]/.freeze
|
||||
|
||||
attr_reader :nplurals, :entry_data, :html_allowed
|
||||
attr_reader :nplurals, :entry_data
|
||||
|
||||
def initialize(entry_data:, nplurals:, html_allowed:)
|
||||
def initialize(entry_data:, nplurals:)
|
||||
@entry_data = entry_data
|
||||
@nplurals = nplurals
|
||||
@html_allowed = html_allowed
|
||||
end
|
||||
|
||||
def msgid
|
||||
|
@ -97,20 +96,6 @@ module Gitlab
|
|||
all_translations.any? { |translation| contains_angle_brackets?(translation) }
|
||||
end
|
||||
|
||||
def msgid_html_allowed?
|
||||
html_allowed.present?
|
||||
end
|
||||
|
||||
def plural_id_html_allowed?
|
||||
html_allowed.present? && html_allowed['plural_id'] == plural_id
|
||||
end
|
||||
|
||||
def translations_html_allowed?
|
||||
msgid_html_allowed? && html_allowed['translations'].present? && all_translations.all? do |translation|
|
||||
html_allowed['translations'].include?(translation)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def contains_angle_brackets?(string)
|
||||
|
|
|
@ -347,6 +347,7 @@ excluded_attributes:
|
|||
- :board_id
|
||||
- :label_id
|
||||
- :milestone_id
|
||||
- :iteration_id
|
||||
epic:
|
||||
- :start_date_sourcing_milestone_id
|
||||
- :due_date_sourcing_milestone_id
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module PerformanceBar
|
||||
class Logger < ::Gitlab::JsonLogger
|
||||
def self.file_name_noext
|
||||
'performance_bar_json'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,7 +5,33 @@ module Gitlab
|
|||
module PerformanceBar
|
||||
module RedisAdapterWhenPeekEnabled
|
||||
def save(request_id)
|
||||
super if ::Gitlab::PerformanceBar.enabled_for_request?
|
||||
return unless ::Gitlab::PerformanceBar.enabled_for_request?
|
||||
return if request_id.blank?
|
||||
|
||||
super
|
||||
|
||||
enqueue_stats_job(request_id)
|
||||
end
|
||||
|
||||
# schedules a job which parses peek profile data and adds them
|
||||
# to a structured log
|
||||
def enqueue_stats_job(request_id)
|
||||
return unless gather_stats?
|
||||
|
||||
@client.sadd(GitlabPerformanceBarStatsWorker::STATS_KEY, request_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
|
||||
return unless uuid = Gitlab::ExclusiveLease.new(
|
||||
GitlabPerformanceBarStatsWorker::LEASE_KEY,
|
||||
timeout: GitlabPerformanceBarStatsWorker::LEASE_TIMEOUT
|
||||
).try_obtain
|
||||
|
||||
GitlabPerformanceBarStatsWorker.perform_in(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid)
|
||||
end
|
||||
|
||||
def gather_stats?
|
||||
return unless Feature.enabled?(:performance_bar_stats)
|
||||
|
||||
Gitlab.com? || !Rails.env.production?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module PerformanceBar
|
||||
# This class fetches Peek stats stored in redis and logs them in a
|
||||
# structured log (so these can be then analyzed in Kibana)
|
||||
class Stats
|
||||
def initialize(redis)
|
||||
@redis = redis
|
||||
end
|
||||
|
||||
def process(id)
|
||||
data = request(id)
|
||||
return unless data
|
||||
|
||||
log_sql_queries(id, data)
|
||||
rescue => err
|
||||
logger.error(message: "failed to process request id #{id}: #{err.message}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request(id)
|
||||
# Peek gem stores request data under peek:requests:request_id key
|
||||
json_data = @redis.get("peek:requests:#{id}")
|
||||
Gitlab::Json.parse(json_data)
|
||||
end
|
||||
|
||||
def log_sql_queries(id, data)
|
||||
return [] unless queries = data.dig('data', 'active-record', 'details')
|
||||
|
||||
queries.each do |query|
|
||||
next unless location = parse_backtrace(query['backtrace'])
|
||||
|
||||
log_info = location.merge(
|
||||
type: :sql,
|
||||
request_id: id,
|
||||
duration_ms: query['duration'].to_f
|
||||
)
|
||||
|
||||
logger.info(log_info)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_backtrace(backtrace)
|
||||
return unless match = /(?<filename>.*):(?<filenum>\d+):in `(?<method>.*)'/.match(backtrace.first)
|
||||
|
||||
{
|
||||
filename: match[:filename],
|
||||
filenum: match[:filenum].to_i,
|
||||
method: match[:method]
|
||||
}
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= Gitlab::PerformanceBar::Logger.build
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -65,10 +65,10 @@ namespace :gettext do
|
|||
linters = files.map do |file|
|
||||
locale = File.basename(File.dirname(file))
|
||||
|
||||
Gitlab::I18n::PoLinter.new(po_path: file, html_todolist: html_todolist, locale: locale)
|
||||
Gitlab::I18n::PoLinter.new(po_path: file, locale: locale)
|
||||
end
|
||||
|
||||
linters.unshift(Gitlab::I18n::PoLinter.new(po_path: pot_file_path, html_todolist: html_todolist))
|
||||
linters.unshift(Gitlab::I18n::PoLinter.new(po_path: pot_file_path))
|
||||
|
||||
failed_linters = linters.select { |linter| linter.errors.any? }
|
||||
|
||||
|
|
1225
locale/en/gitlab.po
1225
locale/en/gitlab.po
File diff suppressed because it is too large
Load Diff
|
@ -371,6 +371,9 @@ msgstr ""
|
|||
msgid "%{authorsName}'s thread"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{board_target} not found"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{code_open}Masked%{code_close} variables are hidden in job logs (though they must match certain regexp requirements to do so)."
|
||||
msgstr ""
|
||||
|
||||
|
@ -9640,6 +9643,9 @@ msgstr ""
|
|||
msgid "DevopsAdoption|Feature adoption is based on usage in the last calendar month. Last updated: %{timestamp}."
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Filter by name"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Issues"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9655,6 +9661,9 @@ msgstr ""
|
|||
msgid "DevopsAdoption|New segment"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|No filter results."
|
||||
msgstr ""
|
||||
|
||||
msgid "DevopsAdoption|Pipelines"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10677,6 +10686,9 @@ msgstr ""
|
|||
msgid "Environments|Deployment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Deployment %{status}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Enable review app"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10794,6 +10806,12 @@ msgstr ""
|
|||
msgid "Environments|This action will run the job defined by %{name} for commit %{linkStart}%{commitId}%{linkEnd} putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Upcoming"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Upcoming deployment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Environments|Updated"
|
||||
msgstr ""
|
||||
|
||||
|
@ -15399,6 +15417,9 @@ msgstr ""
|
|||
msgid "Iteration changed to"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iteration lists not available with your current license"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iteration removed"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24383,9 +24404,15 @@ msgstr ""
|
|||
msgid "SecurityReports|Dismissed '%{vulnerabilityName}'. Turn off the hide dismissed toggle to view."
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Download %{artifactName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Download Report"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Download results"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Either you don't have permission to view this dashboard or the dashboard has not been setup. Please check your permission settings with your administrator or check your dashboard configurations to proceed."
|
||||
msgstr ""
|
||||
|
||||
|
@ -24470,6 +24497,9 @@ msgstr ""
|
|||
msgid "SecurityReports|Security reports help page link"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Security scans have run"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26204,6 +26234,9 @@ msgstr ""
|
|||
msgid "StaticSiteEditor|An error occurred while submitting your changes."
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Automatic formatting changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Branch could not be created."
|
||||
msgstr ""
|
||||
|
||||
|
@ -26222,6 +26255,9 @@ msgstr ""
|
|||
msgid "StaticSiteEditor|Incompatible file content"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Markdown formatting preferences introduced by the Static Site Editor"
|
||||
msgstr ""
|
||||
|
||||
msgid "StaticSiteEditor|Return to site"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ class JobFinder
|
|||
@job_query = options.delete(:job_query)
|
||||
@pipeline_id = options.delete(:pipeline_id)
|
||||
@job_name = options.delete(:job_name)
|
||||
@artifact_path = options.delete(:artifact_path)
|
||||
|
||||
# Force the token to be a string so that if api_token is nil, it's set to '', allowing unauthenticated requests (for forks).
|
||||
api_token = options.delete(:api_token).to_s
|
||||
|
@ -33,19 +34,31 @@ class JobFinder
|
|||
end
|
||||
|
||||
def execute
|
||||
find_job_with_filtered_pipelines || find_job_in_pipeline
|
||||
find_job_with_artifact || find_job_with_filtered_pipelines || find_job_in_pipeline
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :project, :pipeline_query, :job_query, :pipeline_id, :job_name
|
||||
attr_reader :project, :pipeline_query, :job_query, :pipeline_id, :job_name, :artifact_path
|
||||
|
||||
def find_job_with_artifact
|
||||
return if artifact_path.nil?
|
||||
|
||||
Gitlab.pipelines(project, pipeline_query_params).auto_paginate do |pipeline|
|
||||
Gitlab.pipeline_jobs(project, pipeline.id, job_query_params).auto_paginate do |job|
|
||||
return job if found_job_with_artifact?(job) # rubocop:disable Cop/AvoidReturnFromBlocks
|
||||
end
|
||||
end
|
||||
|
||||
raise 'Job not found!'
|
||||
end
|
||||
|
||||
def find_job_with_filtered_pipelines
|
||||
return if pipeline_query.empty?
|
||||
|
||||
Gitlab.pipelines(project, pipeline_query_params).auto_paginate do |pipeline|
|
||||
Gitlab.pipeline_jobs(project, pipeline.id, job_query_params).auto_paginate do |job|
|
||||
return job if job.name == job_name # rubocop:disable Cop/AvoidReturnFromBlocks
|
||||
return job if found_job_by_name?(job) # rubocop:disable Cop/AvoidReturnFromBlocks
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -56,12 +69,22 @@ class JobFinder
|
|||
return unless pipeline_id
|
||||
|
||||
Gitlab.pipeline_jobs(project, pipeline_id, job_query_params).auto_paginate do |job|
|
||||
return job if job.name == job_name # rubocop:disable Cop/AvoidReturnFromBlocks
|
||||
return job if found_job_by_name?(job) # rubocop:disable Cop/AvoidReturnFromBlocks
|
||||
end
|
||||
|
||||
raise 'Job not found!'
|
||||
end
|
||||
|
||||
def found_job_with_artifact?(job)
|
||||
artifact_url = "https://gitlab.com/api/v4/projects/#{CGI.escape(project)}/jobs/#{job.id}/artifacts/#{artifact_path}"
|
||||
response = HTTParty.head(artifact_url) # rubocop:disable Gitlab/HTTParty
|
||||
response.success?
|
||||
end
|
||||
|
||||
def found_job_by_name?(job)
|
||||
job.name == job_name
|
||||
end
|
||||
|
||||
def pipeline_query_params
|
||||
@pipeline_query_params ||= { per_page: 100, **pipeline_query }
|
||||
end
|
||||
|
@ -95,6 +118,10 @@ if $0 == __FILE__
|
|||
options[:job_name] = value
|
||||
end
|
||||
|
||||
opts.on("-a", "--artifact-path ARTIFACT_PATH", String, "A valid artifact path") do |value|
|
||||
options[:artifact_path] = value
|
||||
end
|
||||
|
||||
opts.on("-t", "--api-token API_TOKEN", String, "A value API token with the `read_api` scope") do |value|
|
||||
options[:api_token] = value
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
function retrieve_tests_metadata() {
|
||||
mkdir -p crystalball/ knapsack/ rspec_flaky/ rspec_profiling/
|
||||
mkdir -p knapsack/ rspec_flaky/ rspec_profiling/
|
||||
|
||||
local project_path="gitlab-org/gitlab"
|
||||
local test_metadata_job_id
|
||||
|
@ -16,13 +16,6 @@ function retrieve_tests_metadata() {
|
|||
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
|
||||
scripts/api/download_job_artifact --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
|
||||
fi
|
||||
|
||||
# FIXME: We will need to find a pipeline where the $RSPEC_PACKED_TESTS_MAPPING_PATH.gz actually exists (Crystalball only runs every two-hours, but the `update-tests-metadata` runs for all `master` pipelines...).
|
||||
# if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
|
||||
# (scripts/api/download_job_artifact --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
|
||||
# fi
|
||||
#
|
||||
# scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}"
|
||||
}
|
||||
|
||||
function update_tests_metadata() {
|
||||
|
@ -43,6 +36,21 @@ function update_tests_metadata() {
|
|||
fi
|
||||
}
|
||||
|
||||
function retrieve_tests_mapping() {
|
||||
mkdir -p crystalball/
|
||||
|
||||
local project_path="gitlab-org/gitlab"
|
||||
local test_metadata_with_mapping_job_id
|
||||
|
||||
test_metadata_with_mapping_job_id=$(scripts/api/get_job_id --project "${project_path}" -q "status=success" -q "ref=master" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-tests-metadata" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz")
|
||||
|
||||
if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
|
||||
(scripts/api/download_job_artifact --project "${project_path}" --job-id "${test_metadata_with_mapping_job_id}" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
|
||||
fi
|
||||
|
||||
scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}"
|
||||
}
|
||||
|
||||
function update_tests_mapping() {
|
||||
if ! crystalball_rspec_data_exists; then
|
||||
echo "No crystalball rspec data found."
|
||||
|
@ -119,8 +127,8 @@ function rspec_paralellized_job() {
|
|||
|
||||
local rspec_args="-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}"
|
||||
|
||||
if [[ -n $RSPEC_MATCHING_TESTS_ENABLED ]]; then
|
||||
tooling/bin/parallel_rspec --rspec_args "${rspec_args}" --filter tmp/matching_tests.txt
|
||||
if [[ -n $RSPEC_TESTS_MAPPING_ENABLED ]]; then
|
||||
tooling/bin/parallel_rspec --rspec_args "${rspec_args}" --filter "tmp/matching_tests.txt"
|
||||
else
|
||||
tooling/bin/parallel_rspec --rspec_args "${rspec_args}"
|
||||
fi
|
||||
|
|
|
@ -36,7 +36,7 @@ function install_gitlab_gem() {
|
|||
}
|
||||
|
||||
function install_tff_gem() {
|
||||
gem install test_file_finder --version 0.1.0
|
||||
gem install test_file_finder --version 0.1.1
|
||||
}
|
||||
|
||||
function run_timed_command() {
|
||||
|
|
|
@ -85,20 +85,22 @@ RSpec.describe Boards::ListsController do
|
|||
|
||||
context 'with invalid params' do
|
||||
context 'when label is nil' do
|
||||
it 'returns a not found 404 response' do
|
||||
it 'returns an unprocessable entity 422 response' do
|
||||
create_board_list user: user, board: board, label_id: nil
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
expect(json_response['errors']).to eq(['Label not found'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when label that does not belongs to project' do
|
||||
it 'returns a not found 404 response' do
|
||||
it 'returns an unprocessable entity 422 response' do
|
||||
label = create(:label, name: 'Development')
|
||||
|
||||
create_board_list user: user, board: board, label_id: label.id
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
expect(json_response['errors']).to eq(['Label not found'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -24,6 +24,10 @@ RSpec.describe 'Environments page', :js do
|
|||
'button[title="Stop environment"]'
|
||||
end
|
||||
|
||||
def upcoming_deployment_content_selector
|
||||
'[data-testid="upcoming-deployment-content"]'
|
||||
end
|
||||
|
||||
describe 'page tabs' do
|
||||
it 'shows "Available" and "Stopped" tab with links' do
|
||||
visit_environments(project)
|
||||
|
@ -362,6 +366,26 @@ RSpec.describe 'Environments page', :js do
|
|||
expect(page).to have_content('No deployments yet')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an upcoming deployment' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
|
||||
let!(:deployment) do
|
||||
create(:deployment, :running,
|
||||
environment: environment,
|
||||
sha: project.commit.id)
|
||||
end
|
||||
|
||||
it "renders the upcoming deployment", :aggregate_failures do
|
||||
visit_environments(project)
|
||||
|
||||
within(upcoming_deployment_content_selector) do
|
||||
expect(page).to have_content("##{deployment.iid}")
|
||||
expect(page).to have_selector("a[href=\"#{project_job_path(project, deployment.deployable)}\"]")
|
||||
expect(page).to have_link(href: /#{deployment.user.username}/)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'does have a new environment button' do
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"context": {},
|
||||
"data": {
|
||||
"host": {
|
||||
"hostname": "pc",
|
||||
"canary": null
|
||||
},
|
||||
"active-record": {
|
||||
"duration": "6ms",
|
||||
"calls": "7 (0 cached)",
|
||||
"details": [
|
||||
{
|
||||
"duration": 1.096,
|
||||
"sql": "SELECT COUNT(*) FROM ((SELECT \"badges\".* FROM \"badges\" WHERE \"badges\".\"type\" = 'ProjectBadge' AND \"badges\".\"project_id\" = 8)\nUNION\n(SELECT \"badges\".* FROM \"badges\" WHERE \"badges\".\"type\" = 'GroupBadge' AND \"badges\".\"group_id\" IN (SELECT \"namespaces\".\"id\" FROM \"namespaces\" WHERE \"namespaces\".\"type\" = 'Group' AND \"namespaces\".\"id\" = 28))) badges",
|
||||
"backtrace": [
|
||||
"lib/gitlab/pagination/offset_pagination.rb:53:in `add_pagination_headers'",
|
||||
"lib/gitlab/pagination/offset_pagination.rb:15:in `block in paginate'",
|
||||
"lib/gitlab/pagination/offset_pagination.rb:14:in `tap'",
|
||||
"lib/gitlab/pagination/offset_pagination.rb:14:in `paginate'",
|
||||
"lib/api/helpers/pagination.rb:7:in `paginate'",
|
||||
"lib/api/badges.rb:42:in `block (3 levels) in <class:Badges>'",
|
||||
"ee/lib/gitlab/ip_address_state.rb:10:in `with'",
|
||||
"lib/api/api_guard.rb:208:in `call'",
|
||||
"lib/gitlab/jira/middleware.rb:19:in `call'"
|
||||
],
|
||||
"cached": "",
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"duration": 0.817,
|
||||
"sql": "SELECT \"projects\".* FROM \"projects\" WHERE \"projects\".\"pending_delete\" = $1 AND \"projects\".\"id\" = $2 LIMIT $3",
|
||||
"backtrace": [
|
||||
"lib/api/helpers.rb:112:in `find_project'",
|
||||
"ee/lib/ee/api/helpers.rb:88:in `find_project!'",
|
||||
"lib/api/helpers/members_helpers.rb:14:in `public_send'",
|
||||
"lib/api/helpers/members_helpers.rb:14:in `find_source'",
|
||||
"lib/api/badges.rb:36:in `block (3 levels) in <class:Badges>'",
|
||||
"ee/lib/gitlab/ip_address_state.rb:10:in `with'",
|
||||
"lib/api/api_guard.rb:208:in `call'",
|
||||
"lib/gitlab/jira/middleware.rb:19:in `call'"
|
||||
],
|
||||
"cached": "",
|
||||
"warnings": []
|
||||
},
|
||||
{
|
||||
"duration": 0.817,
|
||||
"sql": "SELECT \"projects\".* FROM \"projects\" WHERE \"projects\".\"pending_delete\" = $1 AND \"projects\".\"id\" = $2 LIMIT $3",
|
||||
"backtrace": [
|
||||
"lib/api/helpers.rb:112:in `find_project'",
|
||||
"ee/lib/ee/api/helpers.rb:88:in `find_project!'",
|
||||
"lib/api/helpers/members_helpers.rb:14:in `public_send'",
|
||||
"lib/api/helpers/members_helpers.rb:14:in `find_source'",
|
||||
"lib/api/badges.rb:36:in `block (3 levels) in <class:Badges>'",
|
||||
"ee/lib/gitlab/ip_address_state.rb:10:in `with'",
|
||||
"lib/api/api_guard.rb:208:in `call'",
|
||||
"lib/gitlab/jira/middleware.rb:19:in `call'"
|
||||
],
|
||||
"cached": "",
|
||||
"warnings": []
|
||||
}
|
||||
],
|
||||
"warnings": []
|
||||
},
|
||||
"gitaly": {
|
||||
"duration": "0ms",
|
||||
"calls": 0,
|
||||
"details": [],
|
||||
"warnings": []
|
||||
},
|
||||
"redis": {
|
||||
"duration": "0ms",
|
||||
"calls": 1,
|
||||
"details": [
|
||||
{
|
||||
"cmd": "get cache:gitlab:flipper/v1/feature/api_kaminari_count_with_limit",
|
||||
"duration": 0.155,
|
||||
"backtrace": [
|
||||
"lib/gitlab/instrumentation/redis_interceptor.rb:30:in `call'",
|
||||
"lib/feature.rb:81:in `enabled?'",
|
||||
"lib/gitlab/pagination/offset_pagination.rb:30:in `paginate_with_limit_optimization'",
|
||||
"lib/gitlab/pagination/offset_pagination.rb:14:in `paginate'",
|
||||
"lib/api/helpers/pagination.rb:7:in `paginate'",
|
||||
"lib/api/badges.rb:42:in `block (3 levels) in <class:Badges>'",
|
||||
"ee/lib/gitlab/ip_address_state.rb:10:in `with'",
|
||||
"lib/api/api_guard.rb:208:in `call'",
|
||||
"lib/gitlab/jira/middleware.rb:19:in `call'"
|
||||
],
|
||||
"storage": "Cache",
|
||||
"warnings": [],
|
||||
"instance": "Cache"
|
||||
}
|
||||
],
|
||||
"warnings": []
|
||||
},
|
||||
"es": {
|
||||
"duration": "0ms",
|
||||
"calls": 0,
|
||||
"details": [],
|
||||
"warnings": []
|
||||
}
|
||||
},
|
||||
"has_warnings": false
|
||||
}
|
||||
|
|
@ -1,3 +1,12 @@
|
|||
export const mockEditorApi = {
|
||||
eventManager: {
|
||||
addEventType: jest.fn(),
|
||||
listen: jest.fn(),
|
||||
removeEventHandler: jest.fn(),
|
||||
},
|
||||
getMarkdown: jest.fn(),
|
||||
};
|
||||
|
||||
export const Editor = {
|
||||
props: {
|
||||
initialValue: {
|
||||
|
@ -18,14 +27,6 @@ export const Editor = {
|
|||
},
|
||||
},
|
||||
created() {
|
||||
const mockEditorApi = {
|
||||
eventManager: {
|
||||
addEventType: jest.fn(),
|
||||
listen: jest.fn(),
|
||||
removeEventHandler: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
this.$emit('load', mockEditorApi);
|
||||
},
|
||||
render(h) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { cloneDeep } from 'lodash';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { format } from 'timeago.js';
|
||||
import EnvironmentItem from '~/environments/components/environment_item.vue';
|
||||
|
@ -30,6 +31,11 @@ describe('Environment item', () => {
|
|||
});
|
||||
|
||||
const findAutoStop = () => wrapper.find('.js-auto-stop');
|
||||
const findUpcomingDeployment = () => wrapper.find('[data-testid="upcoming-deployment"]');
|
||||
const findUpcomingDeploymentContent = () =>
|
||||
wrapper.find('[data-testid="upcoming-deployment-content"]');
|
||||
const findUpcomingDeploymentStatusLink = () =>
|
||||
wrapper.find('[data-testid="upcoming-deployment-status-link"]');
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -87,6 +93,72 @@ describe('Environment item', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('When the envionment has an upcoming deployment', () => {
|
||||
describe('When the upcoming deployment has a deployable', () => {
|
||||
it('should render the build ID and user', () => {
|
||||
expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText(
|
||||
'#27 by upcoming-username',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a status icon with a link and tooltip', () => {
|
||||
expect(findUpcomingDeploymentStatusLink().exists()).toBe(true);
|
||||
|
||||
expect(findUpcomingDeploymentStatusLink().attributes().href).toBe(
|
||||
'/root/environment-test/-/jobs/892',
|
||||
);
|
||||
|
||||
expect(findUpcomingDeploymentStatusLink().attributes().title).toBe(
|
||||
'Deployment running',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the deployment does not have a deployable', () => {
|
||||
beforeEach(() => {
|
||||
const environmentWithoutDeployable = cloneDeep(environment);
|
||||
delete environmentWithoutDeployable.upcoming_deployment.deployable;
|
||||
|
||||
factory({
|
||||
propsData: {
|
||||
model: environmentWithoutDeployable,
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should still renders the build ID and user', () => {
|
||||
expect(findUpcomingDeploymentContent().text()).toMatchInterpolatedText(
|
||||
'#27 by upcoming-username',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not render the status icon', () => {
|
||||
expect(findUpcomingDeploymentStatusLink().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Without upcoming deployment', () => {
|
||||
beforeEach(() => {
|
||||
const environmentWithoutUpcomingDeployment = cloneDeep(environment);
|
||||
delete environmentWithoutUpcomingDeployment.upcoming_deployment;
|
||||
|
||||
factory({
|
||||
propsData: {
|
||||
model: environmentWithoutUpcomingDeployment,
|
||||
canReadEnvironment: true,
|
||||
tableData,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render anything in the upcoming deployment column', () => {
|
||||
expect(findUpcomingDeploymentContent().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Without auto-stop date', () => {
|
||||
beforeEach(() => {
|
||||
factory({
|
||||
|
@ -209,6 +281,10 @@ describe('Environment item', () => {
|
|||
it('should render the number of children in a badge', () => {
|
||||
expect(wrapper.find('.folder-name .badge').text()).toContain(folder.size);
|
||||
});
|
||||
|
||||
it('should not render the "Upcoming deployment" column', () => {
|
||||
expect(findUpcomingDeployment().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When environment can be deleted', () => {
|
||||
|
|
|
@ -86,6 +86,98 @@ const environment = {
|
|||
],
|
||||
deployed_at: '2016-11-29T18:11:58.430Z',
|
||||
},
|
||||
upcoming_deployment: {
|
||||
id: 82,
|
||||
iid: 27,
|
||||
sha: '1132df044b73943943c949e7ac2c2f120a89bf59',
|
||||
ref: {
|
||||
name: 'master',
|
||||
ref_path: '/root/environment-test/-/tree/master',
|
||||
},
|
||||
status: 'running',
|
||||
created_at: '2020-12-04T19:57:49.514Z',
|
||||
deployed_at: null,
|
||||
tag: false,
|
||||
'last?': false,
|
||||
user: {
|
||||
id: 1,
|
||||
name: 'Upcoming Name',
|
||||
username: 'upcoming-username',
|
||||
state: 'active',
|
||||
avatar_url: 'http://0.0.0.0:3000/uploads/-/system/user/avatar/2/avatar.png',
|
||||
web_url: 'http://0.0.0.0:3000/upcoming-username',
|
||||
show_status: false,
|
||||
path: '/upcoming-username',
|
||||
},
|
||||
deployable: {
|
||||
id: 1310,
|
||||
name: 'deploy_to_development',
|
||||
started: '2020-12-04T19:58:10.806Z',
|
||||
archived: false,
|
||||
build_path: '/root/environment-test/-/jobs/892',
|
||||
cancel_path:
|
||||
'/root/environment-test/-/jobs/892/cancel?continue%5Bto%5D=%2Froot%2Fenvironment-test%2F-%2Fjobs%2F892',
|
||||
playable: false,
|
||||
scheduled: false,
|
||||
created_at: '2020-12-04T19:57:49.455Z',
|
||||
updated_at: '2020-12-04T19:58:10.809Z',
|
||||
status: {
|
||||
icon: 'status_running',
|
||||
text: 'running',
|
||||
label: 'running',
|
||||
group: 'running',
|
||||
tooltip: 'running',
|
||||
has_details: true,
|
||||
details_path: '/root/environment-test/-/jobs/892',
|
||||
illustration: {
|
||||
image:
|
||||
'/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
|
||||
size: 'svg-430',
|
||||
title: 'This job does not have a trace.',
|
||||
},
|
||||
favicon:
|
||||
'/assets/ci_favicons/favicon_status_running-9c635b2419a8e1ec991c993061b89cc5aefc0743bb238ecd0c381e7741a70e8c.png',
|
||||
action: {
|
||||
icon: 'cancel',
|
||||
title: 'Cancel',
|
||||
path: '/root/environment-test/-/jobs/892/cancel',
|
||||
method: 'post',
|
||||
button_title: 'Cancel this job',
|
||||
},
|
||||
},
|
||||
},
|
||||
commit: {
|
||||
id: '1132df044b73943943c949e7ac2c2f120a89bf59',
|
||||
short_id: '1132df04',
|
||||
created_at: '2020-12-01T15:46:26.000-05:00',
|
||||
parent_ids: ['e0808dee2a5877563ec140e65d8b41908f90098c'],
|
||||
title: 'Update .gitlab-ci.yml',
|
||||
message: 'Update .gitlab-ci.yml',
|
||||
author_name: 'Upcoming Name',
|
||||
author_email: 'admin@example.com',
|
||||
authored_date: '2020-12-01T15:46:26.000-05:00',
|
||||
committer_name: 'Upcoming Name',
|
||||
committer_email: 'admin@example.com',
|
||||
committed_date: '2020-12-01T15:46:26.000-05:00',
|
||||
web_url:
|
||||
'http://0.0.0.0:3000/root/environment-test/-/commit/1132df044b73943943c949e7ac2c2f120a89bf59',
|
||||
author: {
|
||||
id: 1,
|
||||
name: 'Upcoming Name',
|
||||
username: 'upcoming-username',
|
||||
state: 'active',
|
||||
avatar_url: 'http://0.0.0.0:3000/uploads/-/system/user/avatar/2/avatar.png',
|
||||
web_url: 'http://0.0.0.0:3000/upcoming-username',
|
||||
show_status: false,
|
||||
path: '/upcoming-username',
|
||||
},
|
||||
author_gravatar_url:
|
||||
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
commit_url:
|
||||
'http://0.0.0.0:3000/root/environment-test/-/commit/1132df044b73943943c949e7ac2c2f120a89bf59',
|
||||
commit_path: '/root/environment-test/-/commit/1132df044b73943943c949e7ac2c2f120a89bf59',
|
||||
},
|
||||
},
|
||||
has_stop_action: true,
|
||||
environment_path: 'root/ci-folders/environments/31',
|
||||
log_path: 'root/ci-folders/environments/31/logs',
|
||||
|
@ -156,6 +248,11 @@ const tableData = {
|
|||
title: 'Updated',
|
||||
spacing: 'section-10',
|
||||
},
|
||||
upcoming: {
|
||||
title: 'Upcoming',
|
||||
mobileTitle: 'Upcoming deployment',
|
||||
spacing: 'section-10',
|
||||
},
|
||||
autoStop: {
|
||||
title: 'Auto stop in',
|
||||
spacing: 'section-5',
|
||||
|
|
|
@ -250,4 +250,17 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
|
|||
expect(wrapper.emitted('submit').length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when RichContentEditor component triggers load event', () => {
|
||||
it('stores formatted markdown provided in the event data', () => {
|
||||
const data = { formattedMarkdown: 'formatted markdown' };
|
||||
|
||||
findRichContentEditor().vm.$emit('load', data);
|
||||
|
||||
// We can access the formatted markdown when submitting changes
|
||||
findPublishToolbar().vm.$emit('submit');
|
||||
|
||||
expect(wrapper.emitted('submit')[0][0]).toMatchObject(data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -235,6 +235,7 @@ describe('static_site_editor/pages/home', () => {
|
|||
|
||||
describe('when submitting changes succeeds', () => {
|
||||
const newContent = `new ${content}`;
|
||||
const formattedMarkdown = `formatted ${content}`;
|
||||
|
||||
beforeEach(() => {
|
||||
mutateMock.mockResolvedValueOnce(hasSubmittedChangesMutationPayload).mockResolvedValueOnce({
|
||||
|
@ -243,7 +244,12 @@ describe('static_site_editor/pages/home', () => {
|
|||
},
|
||||
});
|
||||
|
||||
buildWrapper({ content: newContent, images });
|
||||
buildWrapper();
|
||||
|
||||
findEditMetaModal().vm.show = jest.fn();
|
||||
|
||||
findEditArea().vm.$emit('submit', { content: newContent, images, formattedMarkdown });
|
||||
|
||||
findEditMetaModal().vm.$emit('primary', mergeRequestMeta);
|
||||
|
||||
return wrapper.vm.$nextTick();
|
||||
|
@ -266,6 +272,7 @@ describe('static_site_editor/pages/home', () => {
|
|||
variables: {
|
||||
input: {
|
||||
content: newContent,
|
||||
formattedMarkdown,
|
||||
project,
|
||||
sourcePath,
|
||||
username,
|
||||
|
|
|
@ -11,6 +11,8 @@ import {
|
|||
TRACKING_ACTION_CREATE_MERGE_REQUEST,
|
||||
USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
|
||||
USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
|
||||
DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE,
|
||||
DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
|
||||
} from '~/static_site_editor/constants';
|
||||
import generateBranchName from '~/static_site_editor/services/generate_branch_name';
|
||||
import submitContentChanges from '~/static_site_editor/services/submit_content_changes';
|
||||
|
@ -81,6 +83,36 @@ describe('submitContentChanges', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('committing markdown formatting changes', () => {
|
||||
const formattedMarkdown = `formatted ${content}`;
|
||||
const commitPayload = {
|
||||
branch,
|
||||
commit_message: `${DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE}\n\n${DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION}`,
|
||||
actions: [
|
||||
{
|
||||
action: 'update',
|
||||
file_path: sourcePath,
|
||||
content: formattedMarkdown,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('commits markdown formatting changes in a separate commit', () => {
|
||||
return submitContentChanges(buildPayload({ formattedMarkdown })).then(() => {
|
||||
expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, commitPayload);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not commit markdown formatting changes when there are none', () => {
|
||||
return submitContentChanges(buildPayload()).then(() => {
|
||||
expect(Api.commitMultiple.mock.calls).toHaveLength(1);
|
||||
expect(Api.commitMultiple.mock.calls[0][1]).not.toMatchObject({
|
||||
actions: commitPayload.actions,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('commits the content changes to the branch when creating branch succeeds', () => {
|
||||
return submitContentChanges(buildPayload()).then(() => {
|
||||
expect(Api.commitMultiple).toHaveBeenCalledWith(projectId, {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { mockEditorApi } from '@toast-ui/vue-editor';
|
||||
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
|
||||
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
|
||||
import InsertVideoModal from '~/vue_shared/components/rich_content_editor/modals/insert_video_modal.vue';
|
||||
|
@ -114,10 +115,17 @@ describe('Rich Content Editor', () => {
|
|||
});
|
||||
|
||||
describe('when editor is loaded', () => {
|
||||
const formattedMarkdown = 'formatted markdown';
|
||||
|
||||
beforeEach(() => {
|
||||
mockEditorApi.getMarkdown.mockReturnValueOnce(formattedMarkdown);
|
||||
buildWrapper();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockEditorApi.getMarkdown.mockReset();
|
||||
});
|
||||
|
||||
it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
|
||||
expect(addCustomEventListener).toHaveBeenCalledWith(
|
||||
wrapper.vm.editorApi,
|
||||
|
@ -137,6 +145,11 @@ describe('Rich Content Editor', () => {
|
|||
it('registers HTML to markdown renderer', () => {
|
||||
expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi);
|
||||
});
|
||||
|
||||
it('emits load event with the markdown formatted by Toast UI', () => {
|
||||
expect(mockEditorApi.getMarkdown).toHaveBeenCalled();
|
||||
expect(wrapper.emitted('load')[0]).toEqual([{ formattedMarkdown }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when editor is destroyed', () => {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
|
||||
|
||||
describe('SecurityReportDownloadDropdown component', () => {
|
||||
let wrapper;
|
||||
let artifacts;
|
||||
|
||||
const createComponent = props => {
|
||||
wrapper = shallowMount(SecurityReportDownloadDropdown, {
|
||||
propsData: { ...props },
|
||||
});
|
||||
};
|
||||
|
||||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('given report artifacts', () => {
|
||||
beforeEach(() => {
|
||||
artifacts = [
|
||||
{
|
||||
name: 'foo',
|
||||
path: '/foo.json',
|
||||
},
|
||||
{
|
||||
name: 'bar',
|
||||
path: '/bar.json',
|
||||
},
|
||||
];
|
||||
|
||||
createComponent({ artifacts });
|
||||
});
|
||||
|
||||
it('renders a dropdown', () => {
|
||||
expect(findDropdown().props('loading')).toBe(false);
|
||||
});
|
||||
|
||||
it('renders a dropdown items for each artifact', () => {
|
||||
artifacts.forEach((artifact, i) => {
|
||||
const item = findDropdownItems().at(i);
|
||||
expect(item.text()).toContain(artifact.name);
|
||||
expect(item.attributes()).toMatchObject({
|
||||
href: artifact.path,
|
||||
download: expect.any(String),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given it is loading', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ artifacts: [], loading: true });
|
||||
});
|
||||
|
||||
it('renders a loading dropdown', () => {
|
||||
expect(findDropdown().props('loading')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,3 +1,8 @@
|
|||
import {
|
||||
REPORT_TYPE_SAST,
|
||||
REPORT_TYPE_SECRET_DETECTION,
|
||||
} from '~/vue_shared/security_reports/constants';
|
||||
|
||||
export const mockFindings = [
|
||||
{
|
||||
id: null,
|
||||
|
@ -316,3 +321,117 @@ export const secretScanningDiffSuccessMock = {
|
|||
base_report_out_of_date: false,
|
||||
head_report_created_at: '2020-01-10T10:00:00.000Z',
|
||||
};
|
||||
|
||||
export const securityReportDownloadPathsQueryResponse = {
|
||||
project: {
|
||||
mergeRequest: {
|
||||
headPipeline: {
|
||||
id: 'gid://gitlab/Ci::Pipeline/176',
|
||||
jobs: {
|
||||
nodes: [
|
||||
{
|
||||
name: 'secret_detection',
|
||||
artifacts: {
|
||||
nodes: [
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace',
|
||||
fileType: 'TRACE',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection',
|
||||
fileType: 'SECRET_DETECTION',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
],
|
||||
__typename: 'CiJobArtifactConnection',
|
||||
},
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
{
|
||||
name: 'bandit-sast',
|
||||
artifacts: {
|
||||
nodes: [
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace',
|
||||
fileType: 'TRACE',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast',
|
||||
fileType: 'SAST',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
],
|
||||
__typename: 'CiJobArtifactConnection',
|
||||
},
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
{
|
||||
name: 'eslint-sast',
|
||||
artifacts: {
|
||||
nodes: [
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace',
|
||||
fileType: 'TRACE',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
{
|
||||
downloadPath:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast',
|
||||
fileType: 'SAST',
|
||||
__typename: 'CiJobArtifact',
|
||||
},
|
||||
],
|
||||
__typename: 'CiJobArtifactConnection',
|
||||
},
|
||||
__typename: 'CiJob',
|
||||
},
|
||||
],
|
||||
__typename: 'CiJobConnection',
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
__typename: 'MergeRequest',
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* These correspond to SAST jobs in the securityReportDownloadPathsQueryResponse above.
|
||||
*/
|
||||
export const sastArtifacts = [
|
||||
{
|
||||
name: 'bandit-sast',
|
||||
reportType: REPORT_TYPE_SAST,
|
||||
path: '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast',
|
||||
},
|
||||
{
|
||||
name: 'eslint-sast',
|
||||
reportType: REPORT_TYPE_SAST,
|
||||
path: '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast',
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* These correspond to Secret Detection jobs in the securityReportDownloadPathsQueryResponse above.
|
||||
*/
|
||||
export const secretDetectionArtifacts = [
|
||||
{
|
||||
name: 'secret_detection',
|
||||
reportType: REPORT_TYPE_SECRET_DETECTION,
|
||||
path:
|
||||
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection',
|
||||
},
|
||||
];
|
||||
|
||||
export const expectedDownloadDropdownProps = {
|
||||
loading: false,
|
||||
artifacts: [...secretDetectionArtifacts, ...sastArtifacts],
|
||||
};
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { merge } from 'lodash';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import Vuex from 'vuex';
|
||||
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import {
|
||||
expectedDownloadDropdownProps,
|
||||
securityReportDownloadPathsQueryResponse,
|
||||
sastDiffSuccessMock,
|
||||
secretScanningDiffSuccessMock,
|
||||
} from 'jest/vue_shared/security_reports/mock_data';
|
||||
|
@ -15,7 +19,9 @@ import {
|
|||
REPORT_TYPE_SAST,
|
||||
REPORT_TYPE_SECRET_DETECTION,
|
||||
} from '~/vue_shared/security_reports/constants';
|
||||
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
|
||||
import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
|
||||
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
|
@ -47,8 +53,20 @@ describe('Security reports app', () => {
|
|||
);
|
||||
};
|
||||
|
||||
const pendingHandler = () => new Promise(() => {});
|
||||
const successHandler = () => Promise.resolve({ data: securityReportDownloadPathsQueryResponse });
|
||||
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
|
||||
const createMockApolloProvider = handler => {
|
||||
localVue.use(VueApollo);
|
||||
|
||||
const requestHandlers = [[securityReportDownloadPathsQuery, handler]];
|
||||
|
||||
return createMockApollo(requestHandlers);
|
||||
};
|
||||
|
||||
const anyParams = expect.any(Object);
|
||||
|
||||
const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
|
||||
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
|
||||
const findHelpLink = () => wrapper.find('[data-testid="help"]');
|
||||
const setupMockJobArtifact = reportType => {
|
||||
|
@ -103,7 +121,9 @@ describe('Security reports app', () => {
|
|||
});
|
||||
|
||||
it('renders the expected message', () => {
|
||||
expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
|
||||
expect(wrapper.text()).toMatchInterpolatedText(
|
||||
SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance,
|
||||
);
|
||||
});
|
||||
|
||||
describe('clicking the anchor to the pipelines tab', () => {
|
||||
|
@ -172,7 +192,9 @@ describe('Security reports app', () => {
|
|||
});
|
||||
|
||||
it('renders the expected message', () => {
|
||||
expect(wrapper.text()).toMatchInterpolatedText(SecurityReportsApp.i18n.scansHaveRun);
|
||||
expect(wrapper.text()).toMatchInterpolatedText(
|
||||
SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -320,4 +342,118 @@ describe('Security reports app', () => {
|
|||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('given coreSecurityMrWidgetDownloads feature flag is enabled', () => {
|
||||
const createComponentWithFlagEnabled = options =>
|
||||
createComponent(
|
||||
merge(options, {
|
||||
provide: {
|
||||
glFeatures: {
|
||||
coreSecurityMrWidgetDownloads: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
describe('given the query is loading', () => {
|
||||
beforeEach(() => {
|
||||
createComponentWithFlagEnabled({
|
||||
apolloProvider: createMockApolloProvider(pendingHandler),
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Remove this assertion as part of
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
|
||||
it('initially renders nothing', () => {
|
||||
expect(wrapper.isEmpty()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the query loads successfully', () => {
|
||||
beforeEach(() => {
|
||||
createComponentWithFlagEnabled({
|
||||
apolloProvider: createMockApolloProvider(successHandler),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the download dropdown', () => {
|
||||
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
|
||||
});
|
||||
|
||||
it('renders the expected message', () => {
|
||||
const text = wrapper.text();
|
||||
expect(text).not.toContain(SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance);
|
||||
expect(text).toContain(SecurityReportsApp.i18n.scansHaveRun);
|
||||
});
|
||||
|
||||
it('should not render the pipeline tab anchor', () => {
|
||||
expect(findPipelinesTabAnchor().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('given the query fails', () => {
|
||||
beforeEach(() => {
|
||||
createComponentWithFlagEnabled({
|
||||
apolloProvider: createMockApolloProvider(failureHandler),
|
||||
});
|
||||
});
|
||||
|
||||
it('calls createFlash correctly', () => {
|
||||
expect(createFlash).toHaveBeenCalledWith({
|
||||
message: SecurityReportsApp.i18n.apiError,
|
||||
captureError: true,
|
||||
error: expect.any(Error),
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: Remove this assertion as part of
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/273431
|
||||
it('renders nothing', () => {
|
||||
expect(wrapper.isEmpty()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('given coreSecurityMrWidgetCounts and coreSecurityMrWidgetDownloads feature flags are enabled', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet(SAST_COMPARISON_PATH).replyOnce(200, sastDiffSuccessMock);
|
||||
mock.onGet(SECRET_SCANNING_COMPARISON_PATH).replyOnce(200, secretScanningDiffSuccessMock);
|
||||
createComponent({
|
||||
propsData: {
|
||||
sastComparisonPath: SAST_COMPARISON_PATH,
|
||||
secretScanningComparisonPath: SECRET_SCANNING_COMPARISON_PATH,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
coreSecurityMrWidgetCounts: true,
|
||||
coreSecurityMrWidgetDownloads: true,
|
||||
},
|
||||
},
|
||||
apolloProvider: createMockApolloProvider(successHandler),
|
||||
});
|
||||
|
||||
return waitForPromises();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('renders the download dropdown', () => {
|
||||
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
|
||||
});
|
||||
|
||||
it('renders the expected counts message', () => {
|
||||
expect(trimText(wrapper.text())).toContain(
|
||||
'Security scanning detected 3 potential vulnerabilities 2 Critical 1 High and 0 Others',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not render the pipeline tab anchor', () => {
|
||||
expect(findPipelinesTabAnchor().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { extractSecurityReportArtifacts } from '~/vue_shared/security_reports/utils';
|
||||
import {
|
||||
REPORT_TYPE_SAST,
|
||||
REPORT_TYPE_SECRET_DETECTION,
|
||||
} from '~/vue_shared/security_reports/constants';
|
||||
import {
|
||||
securityReportDownloadPathsQueryResponse,
|
||||
sastArtifacts,
|
||||
secretDetectionArtifacts,
|
||||
} from './mock_data';
|
||||
|
||||
describe('extractSecurityReportArtifacts', () => {
|
||||
it.each`
|
||||
reportTypes | expectedArtifacts
|
||||
${[]} | ${[]}
|
||||
${['foo']} | ${[]}
|
||||
${[REPORT_TYPE_SAST]} | ${sastArtifacts}
|
||||
${[REPORT_TYPE_SECRET_DETECTION]} | ${secretDetectionArtifacts}
|
||||
${[REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION]} | ${[...secretDetectionArtifacts, ...sastArtifacts]}
|
||||
`(
|
||||
'returns the expected artifacts given report types $reportTypes',
|
||||
({ reportTypes, expectedArtifacts }) => {
|
||||
expect(
|
||||
extractSecurityReportArtifacts(reportTypes, securityReportDownloadPathsQueryResponse),
|
||||
).toEqual(expectedArtifacts);
|
||||
},
|
||||
);
|
||||
});
|
|
@ -68,9 +68,8 @@ RSpec.describe Mutations::Boards::Lists::Create do
|
|||
context 'when label not found' do
|
||||
let(:list_create_params) { { label_id: "gid://gitlab/Label/#{non_existing_record_id}" } }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }
|
||||
.to raise_error(Gitlab::Graphql::Errors::ArgumentError, 'Label not found!')
|
||||
it 'returns an error' do
|
||||
expect(subject[:errors]).to include 'Label not found'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,4 +40,31 @@ RSpec.describe ::API::Entities::MergeRequestBasic do
|
|||
expect(batch.count).to be_within(3 * query.count).of(control.count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'reviewers' do
|
||||
context "when merge_request_reviewers FF is enabled" do
|
||||
before do
|
||||
stub_feature_flags(merge_request_reviewers: true)
|
||||
merge_request.reviewers = [user]
|
||||
end
|
||||
|
||||
it 'includes assigned reviewers' do
|
||||
result = Gitlab::Json.parse(present(merge_request).to_json)
|
||||
|
||||
expect(result['reviewers'][0]['username']).to eq user.username
|
||||
end
|
||||
end
|
||||
|
||||
context "when merge_request_reviewers FF is disabled" do
|
||||
before do
|
||||
stub_feature_flags(merge_request_reviewers: false)
|
||||
end
|
||||
|
||||
it 'does not include reviewers' do
|
||||
result = Gitlab::Json.parse(present(merge_request).to_json)
|
||||
|
||||
expect(result.keys).not_to include('reviewers')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe ::Gitlab::BackgroundMigration::PopulateDismissedStateForVulnerabilities, schema: 2020_11_30_103926 do
|
||||
let(:users) { table(:users) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:vulnerabilities) { table(:vulnerabilities) }
|
||||
|
||||
let!(:namespace) { namespaces.create!(name: "foo", path: "bar") }
|
||||
let!(:user) { users.create!(name: 'John Doe', email: 'test@example.com', projects_limit: 5) }
|
||||
let!(:project) { projects.create!(namespace_id: namespace.id) }
|
||||
let!(:vulnerability_params) do
|
||||
{
|
||||
project_id: project.id,
|
||||
author_id: user.id,
|
||||
title: 'Vulnerability',
|
||||
severity: 5,
|
||||
confidence: 5,
|
||||
report_type: 5
|
||||
}
|
||||
end
|
||||
|
||||
let!(:vulnerability_1) { vulnerabilities.create!(vulnerability_params.merge(state: 1)) }
|
||||
let!(:vulnerability_2) { vulnerabilities.create!(vulnerability_params.merge(state: 3)) }
|
||||
|
||||
describe '#perform' do
|
||||
it 'changes state of vulnerability to dismissed' do
|
||||
subject.perform(vulnerability_1.id, vulnerability_2.id)
|
||||
|
||||
expect(vulnerability_1.reload.state).to eq(2)
|
||||
expect(vulnerability_2.reload.state).to eq(2)
|
||||
end
|
||||
|
||||
it 'populates missing dismissal information' do
|
||||
expect_next_instance_of(::Gitlab::BackgroundMigration::PopulateMissingVulnerabilityDismissalInformation) do |migration|
|
||||
expect(migration).to receive(:perform).with(vulnerability_1.id, vulnerability_2.id)
|
||||
end
|
||||
|
||||
subject.perform(vulnerability_1.id, vulnerability_2.id)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ require 'simple_po_parser'
|
|||
# Disabling this cop to allow for multi-language examples in comments
|
||||
# rubocop:disable Style/AsciiComments
|
||||
RSpec.describe Gitlab::I18n::PoLinter do
|
||||
let(:linter) { described_class.new(po_path: po_path, html_todolist: {}) }
|
||||
let(:linter) { described_class.new(po_path: po_path) }
|
||||
let(:po_path) { 'spec/fixtures/valid.po' }
|
||||
|
||||
def fake_translation(msgid:, translation:, plural_id: nil, plurals: [])
|
||||
|
@ -24,8 +24,7 @@ RSpec.describe Gitlab::I18n::PoLinter do
|
|||
|
||||
Gitlab::I18n::TranslationEntry.new(
|
||||
entry_data: data,
|
||||
nplurals: plurals.size + 1,
|
||||
html_allowed: nil
|
||||
nplurals: plurals.size + 1
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -160,53 +159,6 @@ RSpec.describe Gitlab::I18n::PoLinter do
|
|||
]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an entry contains html on the todolist' do
|
||||
subject(:linter) { described_class.new(po_path: po_path, html_todolist: todolist) }
|
||||
|
||||
let(:po_path) { 'spec/fixtures/potential_html.po' }
|
||||
let(:todolist) do
|
||||
{
|
||||
'String with a legitimate < use' => {
|
||||
'plural_id' => 'String with lots of < > uses',
|
||||
'translations' => [
|
||||
'Translated string with a legitimate < use',
|
||||
'Translated string with lots of < > uses'
|
||||
]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not present an error' do
|
||||
message_id = 'String with a legitimate < use'
|
||||
|
||||
expect(errors[message_id]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an entry on the html todolist has changed' do
|
||||
subject(:linter) { described_class.new(po_path: po_path, html_todolist: todolist) }
|
||||
|
||||
let(:po_path) { 'spec/fixtures/potential_html.po' }
|
||||
let(:todolist) do
|
||||
{
|
||||
'String with a legitimate < use' => {
|
||||
'plural_id' => 'String with lots of < > uses',
|
||||
'translations' => [
|
||||
'Translated string with a different legitimate < use',
|
||||
'Translated string with lots of < > uses'
|
||||
]
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'presents an error for the changed component' do
|
||||
message_id = 'String with a legitimate < use'
|
||||
|
||||
expect(errors[message_id])
|
||||
.to include a_string_starting_with('translation contains < or >.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_po' do
|
||||
|
@ -276,8 +228,7 @@ RSpec.describe Gitlab::I18n::PoLinter do
|
|||
|
||||
fake_entry = Gitlab::I18n::TranslationEntry.new(
|
||||
entry_data: { msgid: 'the singular', msgid_plural: 'the plural', 'msgstr[0]' => 'the singular' },
|
||||
nplurals: 2,
|
||||
html_allowed: nil
|
||||
nplurals: 2
|
||||
)
|
||||
errors = []
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
describe '#singular_translation' do
|
||||
it 'returns the normal `msgstr` for translations without plural' do
|
||||
data = { msgid: 'Hello world', msgstr: 'Bonjour monde' }
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry.singular_translation).to eq('Bonjour monde')
|
||||
end
|
||||
|
@ -18,7 +18,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
'msgstr[0]' => 'Bonjour monde',
|
||||
'msgstr[1]' => 'Bonjour mondes'
|
||||
}
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry.singular_translation).to eq('Bonjour monde')
|
||||
end
|
||||
|
@ -27,7 +27,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
describe '#all_translations' do
|
||||
it 'returns all translations for singular translations' do
|
||||
data = { msgid: 'Hello world', msgstr: 'Bonjour monde' }
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry.all_translations).to eq(['Bonjour monde'])
|
||||
end
|
||||
|
@ -39,7 +39,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
'msgstr[0]' => 'Bonjour monde',
|
||||
'msgstr[1]' => 'Bonjour mondes'
|
||||
}
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry.all_translations).to eq(['Bonjour monde', 'Bonjour mondes'])
|
||||
end
|
||||
|
@ -52,7 +52,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
msgid_plural: 'Hello worlds',
|
||||
'msgstr[0]' => 'Bonjour monde'
|
||||
}
|
||||
entry = described_class.new(entry_data: data, nplurals: 1, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 1)
|
||||
|
||||
expect(entry.plural_translations).to eq(['Bonjour monde'])
|
||||
end
|
||||
|
@ -65,7 +65,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
'msgstr[1]' => 'Bonjour mondes',
|
||||
'msgstr[2]' => 'Bonjour tous les mondes'
|
||||
}
|
||||
entry = described_class.new(entry_data: data, nplurals: 3, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 3)
|
||||
|
||||
expect(entry.plural_translations).to eq(['Bonjour mondes', 'Bonjour tous les mondes'])
|
||||
end
|
||||
|
@ -77,7 +77,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
msgid: 'hello world',
|
||||
msgstr: 'hello'
|
||||
}
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry).to have_singular_translation
|
||||
end
|
||||
|
@ -89,7 +89,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
"msgstr[0]" => 'hello world',
|
||||
"msgstr[1]" => 'hello worlds'
|
||||
}
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry).to have_singular_translation
|
||||
end
|
||||
|
@ -100,7 +100,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
msgid_plural: 'hello worlds',
|
||||
"msgstr[0]" => 'hello worlds'
|
||||
}
|
||||
entry = described_class.new(entry_data: data, nplurals: 1, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 1)
|
||||
|
||||
expect(entry).not_to have_singular_translation
|
||||
end
|
||||
|
@ -109,7 +109,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
describe '#msgid_contains_newlines' do
|
||||
it 'is true when the msgid is an array' do
|
||||
data = { msgid: %w(hello world) }
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry.msgid_has_multiple_lines?).to be_truthy
|
||||
end
|
||||
|
@ -118,7 +118,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
describe '#plural_id_contains_newlines' do
|
||||
it 'is true when the msgid is an array' do
|
||||
data = { msgid_plural: %w(hello world) }
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry.plural_id_has_multiple_lines?).to be_truthy
|
||||
end
|
||||
|
@ -127,7 +127,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
describe '#translations_contain_newlines' do
|
||||
it 'is true when the msgid is an array' do
|
||||
data = { msgstr: %w(hello world) }
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry.translations_have_multiple_lines?).to be_truthy
|
||||
end
|
||||
|
@ -135,7 +135,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
|
||||
describe '#contains_unescaped_chars' do
|
||||
let(:data) { { msgid: '' } }
|
||||
let(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) }
|
||||
let(:entry) { described_class.new(entry_data: data, nplurals: 2) }
|
||||
|
||||
it 'is true when the msgid is an array' do
|
||||
string = '「100%確定」'
|
||||
|
@ -177,7 +177,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
describe '#msgid_contains_unescaped_chars' do
|
||||
it 'is true when the msgid contains a `%`' do
|
||||
data = { msgid: '「100%確定」' }
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry).to receive(:contains_unescaped_chars?).and_call_original
|
||||
expect(entry.msgid_contains_unescaped_chars?).to be_truthy
|
||||
|
@ -187,7 +187,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
describe '#plural_id_contains_unescaped_chars' do
|
||||
it 'is true when the plural msgid contains a `%`' do
|
||||
data = { msgid_plural: '「100%確定」' }
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry).to receive(:contains_unescaped_chars?).and_call_original
|
||||
expect(entry.plural_id_contains_unescaped_chars?).to be_truthy
|
||||
|
@ -197,7 +197,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
describe '#translations_contain_unescaped_chars' do
|
||||
it 'is true when the translation contains a `%`' do
|
||||
data = { msgstr: '「100%確定」' }
|
||||
entry = described_class.new(entry_data: data, nplurals: 2, html_allowed: nil)
|
||||
entry = described_class.new(entry_data: data, nplurals: 2)
|
||||
|
||||
expect(entry).to receive(:contains_unescaped_chars?).and_call_original
|
||||
expect(entry.translations_contain_unescaped_chars?).to be_truthy
|
||||
|
@ -205,7 +205,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
end
|
||||
|
||||
describe '#msgid_contains_potential_html?' do
|
||||
subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) }
|
||||
subject(:entry) { described_class.new(entry_data: data, nplurals: 2) }
|
||||
|
||||
context 'when there are no angle brackets in the msgid' do
|
||||
let(:data) { { msgid: 'String with no brackets' } }
|
||||
|
@ -225,7 +225,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
end
|
||||
|
||||
describe '#plural_id_contains_potential_html?' do
|
||||
subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) }
|
||||
subject(:entry) { described_class.new(entry_data: data, nplurals: 2) }
|
||||
|
||||
context 'when there are no angle brackets in the plural_id' do
|
||||
let(:data) { { msgid_plural: 'String with no brackets' } }
|
||||
|
@ -245,7 +245,7 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
end
|
||||
|
||||
describe '#translations_contain_potential_html?' do
|
||||
subject(:entry) { described_class.new(entry_data: data, nplurals: 2, html_allowed: nil) }
|
||||
subject(:entry) { described_class.new(entry_data: data, nplurals: 2) }
|
||||
|
||||
context 'when there are no angle brackets in the translations' do
|
||||
let(:data) { { msgstr: 'This string has no angle brackets' } }
|
||||
|
@ -263,78 +263,4 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#msgid_html_allowed?' do
|
||||
subject(:entry) do
|
||||
described_class.new(entry_data: { msgid: 'String with a <strong>' }, nplurals: 2, html_allowed: html_todo)
|
||||
end
|
||||
|
||||
context 'when the html in the string is in the todolist' do
|
||||
let(:html_todo) { { 'plural_id' => nil, 'translations' => [] } }
|
||||
|
||||
it 'returns true' do
|
||||
expect(entry.msgid_html_allowed?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the html in the string is not in the todolist' do
|
||||
let(:html_todo) { nil }
|
||||
|
||||
it 'returns false' do
|
||||
expect(entry.msgid_html_allowed?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#plural_id_html_allowed?' do
|
||||
subject(:entry) do
|
||||
described_class.new(entry_data: { msgid_plural: 'String with many <strong>' }, nplurals: 2, html_allowed: html_todo)
|
||||
end
|
||||
|
||||
context 'when the html in the string is in the todolist' do
|
||||
let(:html_todo) { { 'plural_id' => 'String with many <strong>', 'translations' => [] } }
|
||||
|
||||
it 'returns true' do
|
||||
expect(entry.plural_id_html_allowed?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the html in the string is not in the todolist' do
|
||||
let(:html_todo) { { 'plural_id' => 'String with some <strong>', 'translations' => [] } }
|
||||
|
||||
it 'returns false' do
|
||||
expect(entry.plural_id_html_allowed?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#translations_html_allowed?' do
|
||||
subject(:entry) do
|
||||
described_class.new(entry_data: { msgstr: 'String with a <strong>' }, nplurals: 2, html_allowed: html_todo)
|
||||
end
|
||||
|
||||
context 'when the html in the string is in the todolist' do
|
||||
let(:html_todo) { { 'plural_id' => nil, 'translations' => ['String with a <strong>'] } }
|
||||
|
||||
it 'returns true' do
|
||||
expect(entry.translations_html_allowed?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the html in the string is not in the todolist' do
|
||||
let(:html_todo) { { 'plural_id' => nil, 'translations' => ['String with a different <strong>'] } }
|
||||
|
||||
it 'returns false' do
|
||||
expect(entry.translations_html_allowed?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the todolist only has the msgid' do
|
||||
let(:html_todo) { { 'plural_id' => nil, 'translations' => nil } }
|
||||
|
||||
it 'returns false' do
|
||||
expect(entry.translations_html_allowed?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -656,6 +656,7 @@ boards:
|
|||
lists:
|
||||
- user
|
||||
- milestone
|
||||
- iteration
|
||||
- board
|
||||
- label
|
||||
- list_user_preferences
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled do
|
||||
include ExclusiveLeaseHelpers
|
||||
|
||||
let(:peek_adapter) do
|
||||
Class.new do
|
||||
prepend Gitlab::PerformanceBar::RedisAdapterWhenPeekEnabled
|
||||
|
||||
def initialize(client)
|
||||
@client = client
|
||||
end
|
||||
|
||||
def save(id)
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#save' do
|
||||
let(:client) { double }
|
||||
let(:uuid) { 'foo' }
|
||||
|
||||
before do
|
||||
allow(Gitlab::PerformanceBar).to receive(:enabled_for_request?).and_return(true)
|
||||
end
|
||||
|
||||
it 'stores request id and enqueues stats job' do
|
||||
expect_to_obtain_exclusive_lease(GitlabPerformanceBarStatsWorker::LEASE_KEY, uuid)
|
||||
expect(GitlabPerformanceBarStatsWorker).to receive(:perform_in).with(GitlabPerformanceBarStatsWorker::WORKER_DELAY, uuid)
|
||||
expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid)
|
||||
|
||||
peek_adapter.new(client).save('foo')
|
||||
end
|
||||
|
||||
context 'when performance_bar_stats is disabled' do
|
||||
before do
|
||||
stub_feature_flags(performance_bar_stats: false)
|
||||
end
|
||||
|
||||
it 'ignores stats processing for the request' do
|
||||
expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in)
|
||||
expect(client).not_to receive(:sadd)
|
||||
|
||||
peek_adapter.new(client).save('foo')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when exclusive lease has been already taken' do
|
||||
before do
|
||||
stub_exclusive_lease_taken(GitlabPerformanceBarStatsWorker::LEASE_KEY)
|
||||
end
|
||||
|
||||
it 'stores request id but does not enqueue any job' do
|
||||
expect(GitlabPerformanceBarStatsWorker).not_to receive(:perform_in)
|
||||
expect(client).to receive(:sadd).with(GitlabPerformanceBarStatsWorker::STATS_KEY, uuid)
|
||||
|
||||
peek_adapter.new(client).save('foo')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::PerformanceBar::Stats do
|
||||
describe '#process' do
|
||||
let(:request) { fixture_file('lib/gitlab/performance_bar/peek_data.json') }
|
||||
let(:redis) { double(Gitlab::Redis::SharedState) }
|
||||
let(:logger) { double(Gitlab::PerformanceBar::Logger) }
|
||||
let(:request_id) { 'foo' }
|
||||
let(:stats) { described_class.new(redis) }
|
||||
|
||||
describe '#process' do
|
||||
subject(:process) { stats.process(request_id) }
|
||||
|
||||
before do
|
||||
allow(stats).to receive(:logger).and_return(logger)
|
||||
end
|
||||
|
||||
it 'logs each SQL query including its duration' do
|
||||
allow(redis).to receive(:get).and_return(request)
|
||||
|
||||
expect(logger).to receive(:info)
|
||||
.with({ duration_ms: 1.096, filename: 'lib/gitlab/pagination/offset_pagination.rb',
|
||||
filenum: 53, method: 'add_pagination_headers', request_id: 'foo', type: :sql })
|
||||
expect(logger).to receive(:info)
|
||||
.with({ duration_ms: 0.817, filename: 'lib/api/helpers.rb',
|
||||
filenum: 112, method: 'find_project', request_id: 'foo', type: :sql }).twice
|
||||
|
||||
subject
|
||||
end
|
||||
|
||||
it 'logs an error when the request could not be processed' do
|
||||
allow(redis).to receive(:get).and_return(nil)
|
||||
|
||||
expect(logger).to receive(:error).with(message: anything)
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,27 +5,29 @@ require 'spec_helper'
|
|||
RSpec.describe Boards::Lists::CreateService do
|
||||
describe '#execute' do
|
||||
shared_examples 'creating board lists' do
|
||||
let(:user) { create(:user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
subject(:service) { described_class.new(parent, user, label_id: label.id) }
|
||||
|
||||
before do
|
||||
before_all do
|
||||
parent.add_developer(user)
|
||||
end
|
||||
|
||||
subject(:service) { described_class.new(parent, user, label_id: label.id) }
|
||||
|
||||
context 'when board lists is empty' do
|
||||
it 'creates a new list at beginning of the list' do
|
||||
list = service.execute(board)
|
||||
response = service.execute(board)
|
||||
|
||||
expect(list.position).to eq 0
|
||||
expect(response.success?).to eq(true)
|
||||
expect(response.payload[:list].position).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
context 'when board lists has the done list' do
|
||||
it 'creates a new list at beginning of the list' do
|
||||
list = service.execute(board)
|
||||
response = service.execute(board)
|
||||
|
||||
expect(list.position).to eq 0
|
||||
expect(response.success?).to eq(true)
|
||||
expect(response.payload[:list].position).to eq 0
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -34,9 +36,10 @@ RSpec.describe Boards::Lists::CreateService do
|
|||
create(:list, board: board, position: 0)
|
||||
create(:list, board: board, position: 1)
|
||||
|
||||
list = service.execute(board)
|
||||
response = service.execute(board)
|
||||
|
||||
expect(list.position).to eq 2
|
||||
expect(response.success?).to eq(true)
|
||||
expect(response.payload[:list].position).to eq 2
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -44,32 +47,35 @@ RSpec.describe Boards::Lists::CreateService do
|
|||
it 'creates a new list at end of the label lists' do
|
||||
list1 = create(:list, board: board, position: 0)
|
||||
|
||||
list2 = service.execute(board)
|
||||
list2 = service.execute(board).payload[:list]
|
||||
|
||||
expect(list1.reload.position).to eq 0
|
||||
expect(list2.reload.position).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when provided label does not belongs to the parent' do
|
||||
it 'raises an error' do
|
||||
context 'when provided label does not belong to the parent' do
|
||||
it 'returns an error' do
|
||||
label = create(:label, name: 'in-development')
|
||||
service = described_class.new(parent, user, label_id: label.id)
|
||||
|
||||
expect { service.execute(board) }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
response = service.execute(board)
|
||||
|
||||
expect(response.success?).to eq(false)
|
||||
expect(response.errors).to include('Label not found')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when backlog param is sent' do
|
||||
it 'creates one and only one backlog list' do
|
||||
service = described_class.new(parent, user, 'backlog' => true)
|
||||
list = service.execute(board)
|
||||
list = service.execute(board).payload[:list]
|
||||
|
||||
expect(list.list_type).to eq('backlog')
|
||||
expect(list.position).to be_nil
|
||||
expect(list).to be_valid
|
||||
|
||||
another_backlog = service.execute(board)
|
||||
another_backlog = service.execute(board).payload[:list]
|
||||
|
||||
expect(another_backlog).to eq list
|
||||
end
|
||||
|
@ -77,17 +83,17 @@ RSpec.describe Boards::Lists::CreateService do
|
|||
end
|
||||
|
||||
context 'when board parent is a project' do
|
||||
let(:parent) { create(:project) }
|
||||
let(:board) { create(:board, project: parent) }
|
||||
let(:label) { create(:label, project: parent, name: 'in-progress') }
|
||||
let_it_be(:parent) { create(:project) }
|
||||
let_it_be(:board) { create(:board, project: parent) }
|
||||
let_it_be(:label) { create(:label, project: parent, name: 'in-progress') }
|
||||
|
||||
it_behaves_like 'creating board lists'
|
||||
end
|
||||
|
||||
context 'when board parent is a group' do
|
||||
let(:parent) { create(:group) }
|
||||
let(:board) { create(:board, group: parent) }
|
||||
let(:label) { create(:group_label, group: parent, name: 'in-progress') }
|
||||
let_it_be(:parent) { create(:group) }
|
||||
let_it_be(:board) { create(:board, group: parent) }
|
||||
let_it_be(:label) { create(:group_label, group: parent, name: 'in-progress') }
|
||||
|
||||
it_behaves_like 'creating board lists'
|
||||
end
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabPerformanceBarStatsWorker do
|
||||
include ExclusiveLeaseHelpers
|
||||
|
||||
subject(:worker) { described_class.new }
|
||||
|
||||
describe '#perform' do
|
||||
let(:redis) { double(Gitlab::Redis::SharedState) }
|
||||
let(:uuid) { 1 }
|
||||
|
||||
before do
|
||||
expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis)
|
||||
expect_to_cancel_exclusive_lease(GitlabPerformanceBarStatsWorker::LEASE_KEY, uuid)
|
||||
end
|
||||
|
||||
it 'fetches list of request ids and processes them' do
|
||||
expect(redis).to receive(:smembers).with(GitlabPerformanceBarStatsWorker::STATS_KEY).and_return([1, 2])
|
||||
expect(redis).to receive(:del).with(GitlabPerformanceBarStatsWorker::STATS_KEY)
|
||||
expect_next_instance_of(Gitlab::PerformanceBar::Stats) do |stats|
|
||||
expect(stats).to receive(:process).with(1)
|
||||
expect(stats).to receive(:process).with(2)
|
||||
end
|
||||
|
||||
worker.perform(uuid)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -19,7 +19,12 @@ mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID')
|
|||
mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid)
|
||||
changed_files = mr_changes.changes.map { |change| change['new_path'] }
|
||||
|
||||
mapping = TestFileFinder::Mapping.load('tests.yml')
|
||||
test_files = TestFileFinder::FileFinder.new(paths: changed_files, mapping: mapping).test_files
|
||||
tff = TestFileFinder::FileFinder.new(paths: changed_files).tap do |file_finder|
|
||||
file_finder.use TestFileFinder::MappingStrategies::PatternMatching.load('tests.yml')
|
||||
|
||||
File.write(output_file, test_files.uniq.join(' '))
|
||||
if ENV['RSPEC_TESTS_MAPPING_ENABLED']
|
||||
file_finder.use TestFileFinder::MappingStrategies::DirectMatching.load_json(ENV['RSPEC_TESTS_MAPPING_PATH'])
|
||||
end
|
||||
end
|
||||
|
||||
File.write(output_file, tff.test_files.uniq.join(' '))
|
|
@ -60,7 +60,10 @@ module Tooling
|
|||
end
|
||||
|
||||
def tests_to_run
|
||||
return node_tests if filter_tests.empty?
|
||||
if filter_tests.empty?
|
||||
Knapsack.logger.info 'Running all node tests without filter'
|
||||
return node_tests
|
||||
end
|
||||
|
||||
@tests_to_run ||= node_tests & filter_tests
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue