Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-03 18:11:16 +00:00
parent 7fcda12793
commit 9578c9f9e8
70 changed files with 1436 additions and 146 deletions

View File

@ -24,7 +24,6 @@
/doc/administration/troubleshooting @axil @marcia @mjang1
/doc/ci/ @marcel.amirault @sselhorn
/doc/ci/environments/ @axil
/doc/ci/release/ @axil
/doc/ci/services/ @sselhorn
/doc/ci/test_cases/ @msedlakjakubowski
/doc/development/ @marcia @mjang1
@ -205,15 +204,16 @@ Dangerfile @gl-quality/eng-prod
# Secure & Threat Management ownership delineation
# https://about.gitlab.com/handbook/engineering/development/threat-management/delineate-secure-threat-management.html#technical-boundaries
[Threat Insights]
/app/models/vulnerability.rb @gitlab-org/secure/threat-insights-backend-team
/ee/app/finders/security/ @gitlab-org/secure/threat-insights-backend-team
/ee/app/models/security/ @gitlab-org/secure/threat-insights-backend-team
/ee/app/models/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team
/ee/app/models/vulnerability.rb @gitlab-org/secure/threat-insights-backend-team
/ee/app/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team
/ee/app/policies/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team
/ee/lib/api/vulnerabilit*.rb @gitlab-org/secure/threat-insights-backend-team
/ee/spec/policies/vulnerabilities/ @gitlab-org/secure/threat-insights-backend-team
/ee/spec/policies/vulnerabilities/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team
/ee/spec/policies/vulnerability*.rb @gitlab-org/secure/threat-insights-backend-team
[Secure]
/ee/lib/gitlab/ci/parsers/license_compliance/ @gitlab-org/secure/composition-analysis-be
/ee/lib/gitlab/ci/parsers/security/ @gitlab-org/secure/composition-analysis-be @gitlab-org/secure/dynamic-analysis-be @gitlab-org/secure/static-analysis-be @gitlab-org/secure/fuzzing-be
@ -260,9 +260,7 @@ Dangerfile @gl-quality/eng-prod
[Product Intelligence]
/ee/lib/gitlab/usage_data_counters/ @gitlab-org/growth/product-intelligence/engineers
/ee/lib/ee/gitlab/usage_data.rb @gitlab-org/growth/product-intelligence/engineers
/lib/gitlab/grafana_embed_usage_data.rb @gitlab-org/growth/product-intelligence/engineers
/lib/gitlab/usage_data.rb @gitlab-org/growth/product_intelligence/engineers
/lib/gitlab/cycle_analytics/usage_data.rb @gitlab-org/growth/product-intelligence/engineers
/lib/gitlab/usage_data_counters/ @gitlab-org/growth/product-intelligence/engineers
[Growth Experiments]

View File

@ -162,6 +162,7 @@ setup-test-env:
- tmp/tests/gitaly/gitaly-lfs-smudge
- tmp/tests/gitaly/gitaly-ssh
- tmp/tests/gitaly/internal/
- tmp/tests/gitaly/internal_gitaly2/
- tmp/tests/gitaly/internal_sockets/
- tmp/tests/gitaly/Makefile
- tmp/tests/gitaly/praefect

View File

@ -73,21 +73,19 @@ export default {
</script>
<template>
<div class="gl-display-flex gl-align-items-center gl-h-full">
<div>
<gl-modal
:modal-id="modalId"
:title="$options.modal.title"
:action-primary="$options.modal.actionPrimary"
:action-cancel="$options.modal.actionCancel"
@ok="$emit('deleteSelectedDesigns')"
@ok="$emit('delete-selected-designs')"
>
<p>
{{
s__(
'DesignManagement|Archived designs will still be available in previous versions of the design collection.',
)
}}
</p>
{{
s__(
'DesignManagement|Archived designs will still be available in previous versions of the design collection.',
)
}}
</gl-modal>
<gl-button
v-gl-modal-directive="modalId"

View File

@ -55,6 +55,7 @@ export default {
iid,
}"
:update="updateStoreAfterDelete"
:tag="null"
v-on="$listeners"
>
<slot v-bind="{ mutate, loading, error }"></slot>

View File

@ -130,7 +130,7 @@ export default {
button-icon="archive"
button-category="secondary"
:title="s__('DesignManagement|Archive design')"
@deleteSelectedDesigns="$emit('delete')"
@delete-selected-designs="$emit('delete')"
/>
</header>
</template>

View File

@ -50,7 +50,7 @@ export default {
type="file"
name="design_file"
:accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype"
class="hide"
class="gl-display-none"
multiple
@change="onFileUploadChange"
/>

View File

@ -365,7 +365,8 @@ export default {
v-if="isLatestVersion"
variant="link"
size="small"
class="gl-mr-4 js-select-all"
class="gl-mr-3"
data-testid="select-all-designs-button"
@click="toggleDesignsSelection"
>{{ selectAllButtonText }}
</gl-button>
@ -385,7 +386,7 @@ export default {
data-qa-selector="archive_button"
:loading="loading"
:has-selected-designs="hasSelectedDesigns"
@deleteSelectedDesigns="mutate()"
@delete-selected-designs="mutate()"
>
{{ s__('DesignManagement|Archive selected') }}
</delete-button>

View File

@ -7,6 +7,7 @@ import PipelineGraph from './graph_component.vue';
import {
getQueryHeaders,
reportToSentry,
serializeGqlErr,
toggleQueryPollingByVisibility,
unwrapPipelineData,
} from './utils';
@ -60,8 +61,8 @@ export default {
update(data) {
return unwrapPipelineData(this.pipelineProjectPath, data);
},
error() {
this.reportFailure(LOAD_FAILURE);
error({ gqlError }) {
this.reportFailure(LOAD_FAILURE, serializeGqlErr(gqlError));
},
},
},
@ -112,10 +113,10 @@ export default {
refreshPipelineGraph() {
this.$apollo.queries.pipeline.refetch();
},
reportFailure(type) {
reportFailure(type, err = '') {
this.showAlert = true;
this.alertType = type;
reportToSentry(this.$options.name, this.alertType);
reportToSentry(this.$options.name, `type: ${this.alertType}, info: ${err}`);
},
},
};

View File

@ -6,6 +6,7 @@ import LinkedPipeline from './linked_pipeline.vue';
import {
getQueryHeaders,
reportToSentry,
serializeGqlErr,
toggleQueryPollingByVisibility,
unwrapPipelineData,
validateConfigPaths,
@ -99,12 +100,14 @@ export default {
this.loadingPipelineId = null;
this.$emit('scrollContainer');
},
error(err, _vm, _key, type) {
error({ gqlError }, _vm, _key, type) {
this.$emit('error', LOAD_FAILURE);
reportToSentry(
'linked_pipelines_column',
`error type: ${LOAD_FAILURE}, error: ${err}, apollo error type: ${type}`,
`error type: ${LOAD_FAILURE}, error: ${serializeGqlErr(
gqlError,
)}, apollo error type: ${type}`,
);
},
});

View File

@ -23,7 +23,6 @@ const getQueryHeaders = (etagResource) => {
},
};
};
/* eslint-enable @gitlab/require-i18n-strings */
const reportToSentry = (component, failureType) => {
Sentry.withScope((scope) => {
@ -32,6 +31,25 @@ const reportToSentry = (component, failureType) => {
});
};
const serializeGqlErr = (gqlError) => {
if (!gqlError) {
return 'gqlError data not available.';
}
const { locations, message, path } = gqlError;
return `
${message}.
Locations: ${locations
.flatMap((loc) => Object.entries(loc))
.flat(2)
.join(' ')}.
Path: ${path.join(', ')}.
`;
};
/* eslint-enable @gitlab/require-i18n-strings */
const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => {
const stopStartQuery = (query) => {
if (!Visibility.hidden()) {
@ -82,6 +100,7 @@ const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0;
export {
getQueryHeaders,
reportToSentry,
serializeGqlErr,
toggleQueryPollingByVisibility,
unwrapPipelineData,
validateConfigPaths,

View File

@ -0,0 +1,119 @@
<script>
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import eventHub from '../../event_hub';
import PipelinesArtifactsComponent from './pipelines_artifacts.vue';
import PipelinesManualActions from './pipelines_manual_actions.vue';
export default {
i18n: {
cancelTitle: __('Cancel'),
redeployTitle: __('Retry'),
},
directives: {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
components: {
GlButton,
PipelinesManualActions,
PipelinesArtifactsComponent,
},
props: {
pipeline: {
type: Object,
required: true,
},
cancelingPipeline: {
type: Number,
required: false,
default: null,
},
},
data() {
return {
isRetrying: false,
};
},
computed: {
displayPipelineActions() {
return (
this.pipeline.flags.retryable ||
this.pipeline.flags.cancelable ||
this.pipeline.details.manual_actions.length ||
this.pipeline.details.artifacts.length
);
},
actions() {
if (!this.pipeline || !this.pipeline.details) {
return [];
}
const { details } = this.pipeline;
return [...(details.manual_actions || []), ...(details.scheduled_actions || [])];
},
isCancelling() {
return this.cancelingPipeline === this.pipeline.id;
},
},
watch: {
pipeline() {
this.isRetrying = false;
},
},
methods: {
handleCancelClick() {
eventHub.$emit('openConfirmationModal', {
pipeline: this.pipeline,
endpoint: this.pipeline.cancel_path,
});
},
handleRetryClick() {
this.isRetrying = true;
eventHub.$emit('retryPipeline', this.pipeline.retry_path);
},
},
};
</script>
<template>
<div v-if="displayPipelineActions" class="gl-text-right">
<div class="btn-group">
<pipelines-manual-actions v-if="actions.length > 0" :actions="actions" />
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts"
/>
<gl-button
v-if="pipeline.flags.retryable"
v-gl-tooltip.hover
:aria-label="$options.i18n.redeployTitle"
:title="$options.i18n.redeployTitle"
:disabled="isRetrying"
:loading="isRetrying"
class="js-pipelines-retry-button"
data-qa-selector="pipeline_retry_button"
icon="repeat"
variant="default"
category="secondary"
@click="handleRetryClick"
/>
<gl-button
v-if="pipeline.flags.cancelable"
v-gl-tooltip.hover
v-gl-modal-directive="'confirmation-modal'"
:aria-label="$options.i18n.cancelTitle"
:title="$options.i18n.cancelTitle"
:loading="isCancelling"
:disabled="isCancelling"
icon="close"
variant="danger"
category="primary"
class="js-pipelines-cancel-button"
@click="handleCancelClick"
/>
</div>
</div>
</template>

View File

@ -29,7 +29,7 @@ export default {
};
</script>
<template>
<div :class="classes">
<div :class="classes" data-testid="pipeline-triggerer">
<user-avatar-link
v-if="user"
:link-href="user.path"

View File

@ -61,7 +61,7 @@ export default {
};
</script>
<template>
<div :class="classes">
<div :class="classes" data-testid="pipeline-url-table-cell">
<gl-link
:href="pipeline.path"
data-testid="pipeline-url-link"

View File

@ -0,0 +1,85 @@
<script>
import { CHILD_VIEW } from '~/pipelines/constants';
import CommitComponent from '~/vue_shared/components/commit.vue';
export default {
components: {
CommitComponent,
},
props: {
pipeline: {
type: Object,
required: true,
},
viewType: {
type: String,
required: true,
},
},
computed: {
commitAuthor() {
let commitAuthorInformation;
if (!this.pipeline || !this.pipeline.commit) {
return null;
}
// 1. person who is an author of a commit might be a GitLab user
if (this.pipeline.commit.author) {
// 2. if person who is an author of a commit is a GitLab user
// they can have a GitLab avatar
if (this.pipeline.commit.author.avatar_url) {
commitAuthorInformation = this.pipeline.commit.author;
// 3. If GitLab user does not have avatar, they might have a Gravatar
} else if (this.pipeline.commit.author_gravatar_url) {
commitAuthorInformation = {
...this.pipeline.commit.author,
avatar_url: this.pipeline.commit.author_gravatar_url,
};
}
// 4. If committer is not a GitLab User, they can have a Gravatar
} else {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
}
return commitAuthorInformation;
},
commitTag() {
return this.pipeline?.ref?.tag;
},
commitRef() {
return this.pipeline?.ref;
},
commitUrl() {
return this.pipeline?.commit?.commit_path;
},
commitShortSha() {
return this.pipeline?.commit?.short_id;
},
commitTitle() {
return this.pipeline?.commit?.title;
},
isChildView() {
return this.viewType === CHILD_VIEW;
},
},
};
</script>
<template>
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:merge-request-ref="pipeline.merge_request"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"
:show-ref-info="!isChildView"
/>
</template>

View File

@ -0,0 +1,37 @@
<script>
import { CHILD_VIEW } from '~/pipelines/constants';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
export default {
components: {
CiBadge,
},
props: {
pipeline: {
type: Object,
required: true,
},
viewType: {
type: String,
required: true,
},
},
computed: {
pipelineStatus() {
return this.pipeline?.details?.status ?? {};
},
isChildView() {
return this.viewType === CHILD_VIEW;
},
},
};
</script>
<template>
<ci-badge
:status="pipelineStatus"
:show-text="!isChildView"
:icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status"
/>
</template>

View File

@ -1,15 +1,93 @@
<script>
import { GlTable, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import PipelineOperations from './pipeline_operations.vue';
import PipelineStopModal from './pipeline_stop_modal.vue';
import PipelineTriggerer from './pipeline_triggerer.vue';
import PipelineUrl from './pipeline_url.vue';
import PipelinesCommit from './pipelines_commit.vue';
import PipelinesStatusBadge from './pipelines_status_badge.vue';
import PipelinesTableRowComponent from './pipelines_table_row.vue';
import PipelineStage from './stage.vue';
import PipelinesTimeago from './time_ago.vue';
const DEFAULT_TD_CLASS = 'gl-p-5!';
const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!';
export default {
fields: [
{
key: 'status',
label: s__('Pipeline|Status'),
thClass: DEFAULT_TH_CLASSES,
columnClass: 'gl-w-10p',
tdClass: DEFAULT_TD_CLASS,
thAttr: { 'data-testid': 'status-th' },
},
{
key: 'pipeline',
label: s__('Pipeline|Pipeline'),
thClass: DEFAULT_TH_CLASSES,
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'pipeline-th' },
},
{
key: 'triggerer',
label: s__('Pipeline|Triggerer'),
thClass: DEFAULT_TH_CLASSES,
tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`,
columnClass: 'gl-w-10p',
thAttr: { 'data-testid': 'triggerer-th' },
},
{
key: 'commit',
label: s__('Pipeline|Commit'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'commit-th' },
},
{
key: 'stages',
label: s__('Pipeline|Stages'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'stages-th' },
},
{
key: 'timeago',
label: s__('Pipeline|Duration'),
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-15p',
thAttr: { 'data-testid': 'timeago-th' },
},
{
key: 'actions',
label: '',
thClass: DEFAULT_TH_CLASSES,
tdClass: DEFAULT_TD_CLASS,
columnClass: 'gl-w-20p',
thAttr: { 'data-testid': 'actions-th' },
},
],
components: {
GlTable,
PipelinesTableRowComponent,
PipelinesCommit,
PipelineOperations,
PipelineStage,
PipelinesStatusBadge,
PipelineStopModal,
PipelinesTableRowComponent,
PipelinesTimeago,
PipelineTriggerer,
PipelineUrl,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -43,11 +121,6 @@ export default {
cancelingPipeline: null,
};
},
computed: {
legacyTableClass() {
return !this.glFeatures.newPipelinesTable ? 'ci-table' : '';
},
},
watch: {
pipelines() {
this.cancelingPipeline = null;
@ -73,8 +146,8 @@ export default {
};
</script>
<template>
<div :class="legacyTableClass">
<div v-if="!glFeatures.newPipelinesTable" data-testid="ci-table">
<div class="ci-table">
<div v-if="!glFeatures.newPipelinesTable" data-testid="legacy-ci-table">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 js-pipeline-status" role="rowheader">
{{ s__('Pipeline|Status') }}
@ -107,7 +180,71 @@ export default {
/>
</div>
<gl-table v-else />
<gl-table
v-else
:fields="$options.fields"
:items="pipelines"
tbody-tr-class="commit"
:tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }"
stacked="lg"
fixed
>
<template #head(actions)>
<slot name="table-header-actions"></slot>
</template>
<template #table-colgroup="{ fields }">
<col v-for="field in fields" :key="field.key" :class="field.columnClass" />
</template>
<template #cell(status)="{ item }">
<pipelines-status-badge :pipeline="item" :view-type="viewType" />
</template>
<template #cell(pipeline)="{ item }">
<pipeline-url
class="gl-text-truncate"
:pipeline="item"
:pipeline-schedule-url="pipelineScheduleUrl"
/>
</template>
<template #cell(triggerer)="{ item }">
<pipeline-triggerer :pipeline="item" />
</template>
<template #cell(commit)="{ item }">
<pipelines-commit :pipeline="item" :view-type="viewType" />
</template>
<template #cell(stages)="{ item }">
<div class="stage-cell">
<div></div>
<template v-if="item.details.stages.length > 0">
<div
v-for="(stage, index) in item.details.stages"
:key="index"
class="stage-container dropdown"
data-testid="widget-mini-pipeline-graph"
>
<pipeline-stage
:type="$options.pipelinesTable"
:stage="stage"
:update-dropdown="updateGraphDropdown"
/>
</div>
</template>
</div>
</template>
<template #cell(timeago)="{ item }">
<pipelines-timeago :pipeline="item" />
</template>
<template #cell(actions)="{ item }">
<pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
</template>
</gl-table>
<pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
</div>

View File

@ -33,3 +33,5 @@ export const LOAD_FAILURE = 'load_failure';
export const PARSE_FAILURE = 'parse_failure';
export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
export const CHILD_VIEW = 'child';

View File

@ -11,6 +11,12 @@ export default {
GlIcon,
},
props: {
showHeader: {
type: Boolean,
required: false,
default: true,
},
sectionTitle: {
type: String,
required: true,
@ -84,7 +90,7 @@ export default {
<template>
<div>
<gl-dropdown-section-header>
<gl-dropdown-section-header v-if="showHeader">
<div class="gl-display-flex align-items-center" data-testid="section-header">
<span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span>
<gl-badge variant="neutral">{{ totalCountText }}</gl-badge>

View File

@ -8,9 +8,16 @@ import {
GlIcon,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { debounce, isArray } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import { SEARCH_DEBOUNCE_MS, DEFAULT_I18N } from '../constants';
import {
ALL_REF_TYPES,
SEARCH_DEBOUNCE_MS,
DEFAULT_I18N,
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
} from '../constants';
import createStore from '../stores';
import RefResultsSection from './ref_results_section.vue';
@ -28,6 +35,20 @@ export default {
RefResultsSection,
},
props: {
enabledRefTypes: {
type: Array,
required: false,
default: () => ALL_REF_TYPES,
validator: (val) =>
// It has to be an arrray
isArray(val) &&
// with at least one item
val.length > 0 &&
// and only "REF_TYPE_BRANCHES", "REF_TYPE_TAGS", and "REF_TYPE_COMMITS" are allowed
val.every((item) => ALL_REF_TYPES.includes(item)) &&
// and no duplicates are allowed
val.length === new Set(val).size,
},
value: {
type: String,
required: false,
@ -62,17 +83,29 @@ export default {
};
},
showBranchesSection() {
return Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error);
return (
this.enabledRefTypes.includes(REF_TYPE_BRANCHES) &&
Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error)
);
},
showTagsSection() {
return Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error);
return (
this.enabledRefTypes.includes(REF_TYPE_TAGS) &&
Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error)
);
},
showCommitsSection() {
return Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error);
return (
this.enabledRefTypes.includes(REF_TYPE_COMMITS) &&
Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error)
);
},
showNoResults() {
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
},
showSectionHeaders() {
return this.enabledRefTypes.length > 1;
},
},
watch: {
// Keep the Vuex store synchronized if the parent
@ -97,10 +130,18 @@ export default {
}, SEARCH_DEBOUNCE_MS);
this.setProjectId(this.projectId);
this.search(this.query);
this.$watch(
'enabledRefTypes',
() => {
this.setEnabledRefTypes(this.enabledRefTypes);
this.search(this.query);
},
{ immediate: true },
);
},
methods: {
...mapActions(['setProjectId', 'setSelectedRef', 'search']),
...mapActions(['setEnabledRefTypes', 'setProjectId', 'setSelectedRef', 'search']),
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
@ -170,6 +211,7 @@ export default {
:selected-ref="selectedRef"
:error="matches.branches.error"
:error-message="i18n.branchesErrorMessage"
:show-header="showSectionHeaders"
data-testid="branches-section"
@selected="selectRef($event)"
/>
@ -185,6 +227,7 @@ export default {
:selected-ref="selectedRef"
:error="matches.tags.error"
:error-message="i18n.tagsErrorMessage"
:show-header="showSectionHeaders"
data-testid="tags-section"
@selected="selectRef($event)"
/>
@ -200,6 +243,7 @@ export default {
:selected-ref="selectedRef"
:error="matches.commits.error"
:error-message="i18n.commitsErrorMessage"
:show-header="showSectionHeaders"
data-testid="commits-section"
@selected="selectRef($event)"
/>

View File

@ -1,5 +1,10 @@
import { __ } from '~/locale';
export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES';
export const REF_TYPE_TAGS = 'REF_TYPE_TAGS';
export const REF_TYPE_COMMITS = 'REF_TYPE_COMMITS';
export const ALL_REF_TYPES = Object.freeze([REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]);
export const X_TOTAL_HEADER = 'x-total';
export const SEARCH_DEBOUNCE_MS = 250;

View File

@ -1,17 +1,26 @@
import Api from '~/api';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS } from '../constants';
import * as types from './mutation_types';
export const setEnabledRefTypes = ({ commit }, refTypes) =>
commit(types.SET_ENABLED_REF_TYPES, refTypes);
export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId);
export const setSelectedRef = ({ commit }, selectedRef) =>
commit(types.SET_SELECTED_REF, selectedRef);
export const search = ({ dispatch, commit }, query) => {
export const search = ({ state, dispatch, commit }, query) => {
commit(types.SET_QUERY, query);
dispatch('searchBranches');
dispatch('searchTags');
dispatch('searchCommits');
const dispatchIfRefTypeEnabled = (refType, action) => {
if (state.enabledRefTypes.includes(refType)) {
dispatch(action);
}
};
dispatchIfRefTypeEnabled(REF_TYPE_BRANCHES, 'searchBranches');
dispatchIfRefTypeEnabled(REF_TYPE_TAGS, 'searchTags');
dispatchIfRefTypeEnabled(REF_TYPE_COMMITS, 'searchCommits');
};
export const searchBranches = ({ commit, state }) => {

View File

@ -1,3 +1,5 @@
export const SET_ENABLED_REF_TYPES = 'SET_ENABLED_REF_TYPES';
export const SET_PROJECT_ID = 'SET_PROJECT_ID';
export const SET_SELECTED_REF = 'SET_SELECTED_REF';
export const SET_QUERY = 'SET_QUERY';

View File

@ -4,6 +4,9 @@ import { X_TOTAL_HEADER } from '../constants';
import * as types from './mutation_types';
export default {
[types.SET_ENABLED_REF_TYPES](state, refTypes) {
state.enabledRefTypes = refTypes;
},
[types.SET_PROJECT_ID](state, projectId) {
state.projectId = projectId;
},

View File

@ -1,23 +1,18 @@
const createRefTypeState = () => ({
list: [],
totalCount: 0,
error: null,
});
export default () => ({
enabledRefTypes: [],
projectId: null,
query: '',
matches: {
branches: {
list: [],
totalCount: 0,
error: null,
},
tags: {
list: [],
totalCount: 0,
error: null,
},
commits: {
list: [],
totalCount: 0,
error: null,
},
branches: createRefTypeState(),
tags: createRefTypeState(),
commits: createRefTypeState(),
},
selectedRef: null,
requestCount: 0,

View File

@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:codequality_backend_comparison, @project, default_enabled: :yaml)
push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml)
record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b)

View File

@ -46,6 +46,8 @@ class RootController < Dashboard::ProjectsController
redirect_to(activity_dashboard_path)
when 'starred_project_activity'
redirect_to(activity_dashboard_path(filter: 'starred'))
when 'followed_user_activity'
redirect_to(activity_dashboard_path(filter: 'followed'))
when 'groups'
redirect_to(dashboard_groups_path)
when 'todos'

View File

@ -29,6 +29,7 @@ module PreferencesHelper
stars: _("Starred Projects"),
project_activity: _("Your Projects' Activity"),
starred_project_activity: _("Starred Projects' Activity"),
followed_user_activity: _("Followed Users' Activity"),
groups: _("Your Groups"),
todos: _("Your To-Do List"),
issues: _("Assigned Issues"),

View File

@ -272,7 +272,7 @@ class User < ApplicationRecord
enum layout: { fixed: 0, fluid: 1 }
# User's Dashboard preference
enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8 }
enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8, followed_user_activity: 9 }
# User's Project preference
enum project_view: { readme: 0, activity: 1, files: 2 }

View File

@ -25,7 +25,11 @@
&bull;
- if total_count > recent_releases.count
&bull;
= link_to n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }, project_releases_path(milestone.project)
- more_text = n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }
- if milestone.project_milestone?
= link_to more_text, project_releases_path(milestone.project)
- else
= more_text
%div
= render('shared/milestone_expired', milestone: milestone)
- if milestone.group_milestone?

View File

@ -0,0 +1,5 @@
---
title: "Add 'Followed User Activity' as dashboard user choices"
merge_request: 55165
author: Benj Fassbind @randombenj
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix button alignment in design management header
merge_request: 48003
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add rake task to cleanup description templates cache in batches
merge_request: 54706
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add runners api context metadata
merge_request: 55089
author:
type: changed

View File

@ -13,13 +13,13 @@ GitLab provides Rake tasks for general maintenance.
This command gathers information about your GitLab installation and the system it runs on.
These may be useful when asking for help or reporting issues.
**Omnibus Installation**
**For Omnibus installations**
```shell
sudo gitlab-rake gitlab:env:info
```
**Source Installation**
**For installations from source**
```shell
bundle exec rake gitlab:env:info RAILS_ENV=production
@ -76,13 +76,13 @@ installations: a license cannot be installed into GitLab Community Edition.
These may be useful when raising tickets with Support, or for programmatically
checking your license parameters.
**Omnibus Installation**
**For Omnibus installations**
```shell
sudo gitlab-rake gitlab:license:info
```
**Source Installation**
**For installations from source**
```shell
bundle exec rake gitlab:license:info RAILS_ENV=production
@ -119,13 +119,13 @@ You may also have a look at our troubleshooting guides for:
To run `gitlab:check`, run:
**Omnibus Installation**
**For Omnibus installations**
```shell
sudo gitlab-rake gitlab:check
```
**Source Installation**
**For installations from source**
```shell
bundle exec rake gitlab:check RAILS_ENV=production
@ -182,13 +182,13 @@ Checking GitLab ... Finished
In some case it is necessary to rebuild the `authorized_keys` file. To do this, run:
**Omnibus Installation**
**For Omnibus installations**
```shell
sudo gitlab-rake gitlab:shell:setup
```
**Source Installation**
**For installations from source**
```shell
cd /home/git/gitlab
@ -203,18 +203,64 @@ You will lose any data stored in authorized_keys file.
Do you want to continue (yes/no)? yes
```
## Clear issue and merge request description template names cache
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54706) in GitLab 13.10.
If the issue or merge request description template names in the dropdown
do not reflect the actual description template names in the repository, consider clearing
the Redis cache that stores the template names information.
You can clear the cache of
[all issues and merge request templates in the installation](#clear-cache-for-all-issue-and-merge-request-template-names)
or [in a specific project](#clear-cache-for-issue-and-merge-request-template-names-in-specific-projects).
### Clear cache for all issue and merge request template names
If you want to refresh issue and merge request templates for all projects:
**For Omnibus installations**
```shell
sudo gitlab-rake cache:clear:description_templates
```
**For installations from source**
```shell
cd /home/git/gitlab
sudo -u git -H bundle exec rake cache:clear:description_templates RAILS_ENV=production
```
### Clear cache for issue and merge request template names in specific projects
If you want to refresh issue and merge request templates for specific projects,
provide a comma-separated list of IDs as the `project_ids` parameter to the Rake task.
**For Omnibus installations**
```shell
sudo gitlab-rake cache:clear:description_templates project_ids=10,25,35
```
**For installations from source**
```shell
cd /home/git/gitlab
sudo -u git -H bundle exec rake cache:clear:description_templates project_ids=10,25,35 RAILS_ENV=production
```
## Clear Redis cache
If for some reason the dashboard displays the wrong information, you might want to
clear Redis' cache. To do this, run:
**Omnibus Installation**
**For Omnibus installations**
```shell
sudo gitlab-rake cache:clear
```
**Source Installation**
**For installations from source**
```shell
cd /home/git/gitlab
@ -229,7 +275,7 @@ missing some icons. In that case, try to precompile the assets again.
This only applies to source installations and does NOT apply to
Omnibus packages.
**Source Installation**
**For installations from source**
```shell
cd /home/git/gitlab
@ -249,13 +295,13 @@ Sometimes you need to know if your GitLab installation can connect to a TCP
service on another machine - perhaps a PostgreSQL or HTTPS server. A Rake task
is included to help you with this:
**Omnibus Installation**
**For Omnibus installations**
```shell
sudo gitlab-rake gitlab:tcp_check[example.com,80]
```
**Source Installation**
**For installations from source**
```shell
cd /home/git/gitlab

View File

@ -171,8 +171,7 @@ certain arguments may also increase the complexity of a query.
NOTE:
The complexity limits may be revised in future, and additionally, the complexity
of a query may be altered. Changes to complexity can happen on `X.0` or `X.6`
releases without a deprecation period.
of a query may be altered.
### Request timeout

View File

@ -30,10 +30,10 @@ Metrics for a branch are read from the latest metrics report artifact (default f
For an MR, the values of these metrics from the feature branch are compared to the values from the target branch. Then they are displayed in the MR widget in this order:
- Metrics that have been added by the MR. Marked with a **New** badge.
- Existing metrics with changed values.
- Existing metrics with unchanged values.
- Metrics that have been added by the MR. Marked with a **New** badge.
- Metrics that have been removed by the MR. Marked with a **Removed** badge.
- Existing metrics with unchanged values.
## How to set it up

View File

@ -135,12 +135,12 @@ These variables are available when:
| `CI_MERGE_REQUEST_PROJECT_URL` | 11.6 | all | The URL of the project of the merge request. For example, `http://192.168.10.15:3000/namespace/awesome-project`. |
| `CI_MERGE_REQUEST_REF_PATH` | 11.6 | all | The ref path of the merge request. For example, `refs/merge-requests/1/head`. |
| `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` | 11.6 | all | The source branch name of the merge request. |
| `CI_MERGE_REQUEST_SOURCE_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the source branch of the merge request. Only available in [merged results pipelines](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
| `CI_MERGE_REQUEST_SOURCE_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the source branch of the merge request. The variable is empty in merge request pipelines. The SHA is present only in [merged results pipelines](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
| `CI_MERGE_REQUEST_SOURCE_PROJECT_ID` | 11.6 | all | The ID of the source project of the merge request. |
| `CI_MERGE_REQUEST_SOURCE_PROJECT_PATH` | 11.6 | all | The path of the source project of the merge request. |
| `CI_MERGE_REQUEST_SOURCE_PROJECT_URL` | 11.6 | all | The URL of the source project of the merge request. |
| `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` | 11.6 | all | The target branch name of the merge request. |
| `CI_MERGE_REQUEST_TARGET_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the target branch of the merge request. Only available in [merged results pipelines](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
| `CI_MERGE_REQUEST_TARGET_BRANCH_SHA` | 11.9 | all | The HEAD SHA of the target branch of the merge request. The variable is empty in merge request pipelines. The SHA is present only in [merged results pipelines](../merge_request_pipelines/pipelines_for_merged_results/index.md). **(PREMIUM)** |
| `CI_MERGE_REQUEST_TITLE` | 11.9 | all | The title of the merge request. |
| `CI_MERGE_REQUEST_EVENT_TYPE` | 12.3 | all | The event type of the merge request. Can be `detached`, `merged_result` or `merge_train`. |
| `CI_MERGE_REQUEST_DIFF_ID` | 13.7 | all | The version of the merge request diff. |

View File

@ -1,6 +1,6 @@
---
stage: none
group: unassigned
stage: Manage
group: Access
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
type: reference, howto, concepts
---

View File

@ -40,7 +40,7 @@ Prerequisites:
- You need to [authenticate with the API](../../../api/README.md#authentication). If authenticating with a deploy token, it must be configured with the `write_package_registry` scope.
```plaintext
PUT /projects/:id/packages/generic/:package_name/:package_version/:file_name
PUT /projects/:id/packages/generic/:package_name/:package_version/:file_name?status=:status
```
| Attribute | Type | Required | Description |
@ -58,7 +58,7 @@ Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" \
--upload-file path/to/file.txt \
"https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt"
"https://gitlab.example.com/api/v4/projects/24/packages/generic/my_package/0.0.1/file.txt?status=hidden"
```
Example response:

View File

@ -108,12 +108,13 @@ select few, the amount of activity on the default Dashboard page can be
overwhelming. Changing this setting allows you to redefine your default
dashboard.
You have 8 options here that you can use for your default dashboard view:
You can include the following options for your default dashboard view:
- Your projects (default)
- Starred projects
- Your projects' activity
- Starred projects' activity
- Followed Users' Activity
- Your groups
- Your [To-Do List](../todos.md)
- Assigned Issues

View File

@ -34,12 +34,12 @@ module API
if runner_registration_token_valid?
# Create shared runner. Requires admin access
attributes.merge(runner_type: :instance_type)
elsif project = Project.find_by_runners_token(params[:token])
elsif @project = Project.find_by_runners_token(params[:token])
# Create a specific runner for the project
attributes.merge(runner_type: :project_type, projects: [project])
elsif group = Group.find_by_runners_token(params[:token])
attributes.merge(runner_type: :project_type, projects: [@project])
elsif @group = Group.find_by_runners_token(params[:token])
# Create a specific runner for the group
attributes.merge(runner_type: :group_type, groups: [group])
attributes.merge(runner_type: :group_type, groups: [@group])
else
forbidden!
end
@ -81,12 +81,7 @@ module API
end
resource :jobs do
before do
Gitlab::ApplicationContext.push(
user: -> { current_job&.user },
project: -> { current_job&.project }
)
end
before { set_application_context }
desc 'Request a job' do
success Entities::JobRequest::Response

View File

@ -71,6 +71,26 @@ module API
header 'Job-Status', job.status
forbidden!(reason)
end
def set_application_context
if current_job
Gitlab::ApplicationContext.push(
user: -> { current_job.user },
project: -> { current_job.project }
)
elsif current_runner&.project_type?
Gitlab::ApplicationContext.push(
project: -> do
projects = current_runner.projects.limit(2) # rubocop: disable CodeReuse/ActiveRecord
projects.first if projects.length == 1
end
)
elsif current_runner&.group_type?
Gitlab::ApplicationContext.push(
namespace: -> { current_runner.groups.first }
)
end
end
end
end
end

View File

@ -9,6 +9,8 @@ module Gitlab
class ProjectPipelineStatus
include Gitlab::Utils::StrongMemoize
ALL_PIPELINES_STATUS_PATTERN = 'projects/*/pipeline_status'
attr_accessor :sha, :status, :ref, :project, :loaded
def self.load_for_project(project)

View File

@ -52,7 +52,7 @@ module Gitlab
return unless included?
strong_memoize(:errors) do
needs_errors
[needs_errors, variable_expansion_errors].compact.flatten
end
end
@ -153,6 +153,12 @@ module Gitlab
@pipeline.project.actual_limits.ci_needs_size_limit
end
def variable_expansion_errors
sorted_collection = evaluate_context.variables.sorted_collection(@pipeline.project)
errors = sorted_collection.errors
["#{name}: #{errors}"] if errors
end
def pipeline_attributes
{
pipeline: @pipeline,

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
module Gitlab
module Cleanup
module Redis
class BatchDeleteByPattern
REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan
attr_reader :patterns
def initialize(patterns)
raise ArgumentError.new('Argument should be an Array of patterns') unless patterns.is_a?(Array)
@patterns = patterns
end
def execute
return if patterns.blank?
batch_delete_cache_keys
end
private
def batch_delete_cache_keys
Gitlab::Redis::Cache.with do |redis|
patterns.each do |match|
cursor = REDIS_SCAN_START_STOP
loop do
cursor, keys = redis.scan(
cursor,
match: match,
count: REDIS_CLEAR_BATCH_SIZE
)
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.del(*keys) if keys.any?
end
break if cursor == REDIS_SCAN_START_STOP
end
end
end
end
end
end
end
end

View File

@ -0,0 +1,88 @@
# frozen_string_literal: true
module Gitlab
module Cleanup
module Redis
class DescriptionTemplatesCacheKeysPatternBuilder
# project_ids - a list of project_ids for which to compute description templates cache keys or `:all` to compute
# a pattern that cover all description templates cache keys.
#
# Example
# * ::Gitlab::Cleanup::Redis::BatchDeleteDescriptionTemplates.new(:all).execute - to get 2
# patterns for all issue and merge request description templates cache keys.
#
# * ::Gitlab::Cleanup::Redis::BatchDeleteDescriptionTemplates.new([1,2,3,4]).execute - to get an array of
# patterns for each project's issue and merge request description templates cache keys.
def initialize(project_ids)
raise ArgumentError.new('project_ids can either be an array of project IDs or :all') if project_ids != :all && !project_ids.is_a?(Array)
@project_ids = parse_project_ids(project_ids)
end
def execute
case project_ids
when :all
all_instance_patterns
else
project_patterns
end
end
private
attr_reader :project_ids
def parse_project_ids(project_ids)
return project_ids if project_ids == :all
project_ids.map { |id| Integer(id) }
rescue ArgumentError
raise ArgumentError.new('Invalid Project ID. Please ensure all passed in project ids values are valid integer project ids.')
end
def project_patterns
cache_key_patterns = []
Project.id_in(project_ids).each_batch do |batch|
cache_key_patterns << batch.map do |pr|
next unless pr.repository.exists?
cache = Gitlab::RepositoryCache.new(pr.repository)
[repo_issue_templates_cache_key(cache), repo_merge_request_templates_cache_key(cache)]
end
end
cache_key_patterns.flatten.compact
end
def all_instance_patterns
[all_issue_templates_cache_key, all_merge_request_templates_cache_key]
end
def issue_templates_cache_key
Repository::METHOD_CACHES_FOR_FILE_TYPES[:issue_template]
end
def merge_request_templates_cache_key
Repository::METHOD_CACHES_FOR_FILE_TYPES[:merge_request_template]
end
def all_issue_templates_cache_key
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:#{issue_templates_cache_key}:*"
end
def all_merge_request_templates_cache_key
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:#{merge_request_templates_cache_key}:*"
end
def repo_issue_templates_cache_key(cache)
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:#{cache.cache_key(issue_templates_cache_key)}"
end
def repo_merge_request_templates_cache_key(cache)
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:#{cache.cache_key(merge_request_templates_cache_key)}"
end
end
end
end
end

View File

@ -115,7 +115,7 @@ module Gitlab
config[:storage] = storages
internal_socket_dir = File.join(gitaly_dir, 'internal_sockets')
internal_socket_dir = options[:internal_socket_dir] || File.join(gitaly_dir, 'internal_sockets')
FileUtils.mkdir(internal_socket_dir) unless File.exist?(internal_socket_dir)
config[:internal_socket_dir] = internal_socket_dir

View File

@ -2,32 +2,22 @@
namespace :cache do
namespace :clear do
REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
REDIS_SCAN_START_STOP = '0'.freeze # Magic value, see http://redis.io/commands/scan
desc "GitLab | Cache | Clear redis cache"
task redis: :environment do
Gitlab::Redis::Cache.with do |redis|
cache_key_pattern = %W[#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*
projects/*/pipeline_status]
cache_key_patterns = %W[
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}*
#{Gitlab::Cache::Ci::ProjectPipelineStatus::ALL_PIPELINES_STATUS_PATTERN}
]
cache_key_pattern.each do |match|
cursor = REDIS_SCAN_START_STOP
loop do
cursor, keys = redis.scan(
cursor,
match: match,
count: REDIS_CLEAR_BATCH_SIZE
)
::Gitlab::Cleanup::Redis::BatchDeleteByPattern.new(cache_key_patterns).execute
end
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.del(*keys) if keys.any?
end
desc "GitLab | Cache | Clear description templates redis cache"
task description_templates: :environment do
project_ids = Array(ENV['project_ids']&.split(',')).map!(&:squish)
break if cursor == REDIS_SCAN_START_STOP
end
end
end
cache_key_patterns = ::Gitlab::Cleanup::Redis::DescriptionTemplatesCacheKeysPatternBuilder.new(project_ids).execute
::Gitlab::Cleanup::Redis::BatchDeleteByPattern.new(cache_key_patterns).execute
end
task all: [:redis]

View File

@ -13201,6 +13201,9 @@ msgstr ""
msgid "Follow"
msgstr ""
msgid "Followed Users' Activity"
msgstr ""
msgid "Followed users"
msgstr ""

View File

@ -68,6 +68,18 @@ RSpec.describe RootController do
end
end
context 'who has customized their dashboard setting for followed user activities' do
before do
user.dashboard = 'followed_user_activity'
end
it 'redirects to the activity list' do
get :index
expect(response).to redirect_to activity_dashboard_path(filter: 'followed')
end
end
context 'who has customized their dashboard setting for groups' do
before do
user.dashboard = 'groups'

View File

@ -36,7 +36,7 @@ describe('Batch delete button component', () => {
expect(findButton().attributes('disabled')).toBeTruthy();
});
it('emits `deleteSelectedDesigns` event on modal ok click', () => {
it('emits `delete-selected-designs` event on modal ok click', () => {
createComponent();
findButton().vm.$emit('click');
return wrapper.vm
@ -46,7 +46,7 @@ describe('Batch delete button component', () => {
return wrapper.vm.$nextTick();
})
.then(() => {
expect(wrapper.emitted().deleteSelectedDesigns).toBeTruthy();
expect(wrapper.emitted('delete-selected-designs')).toBeTruthy();
});
});

View File

@ -106,11 +106,11 @@ describe('Design management toolbar component', () => {
});
});
it('emits `delete` event on deleteButton `deleteSelectedDesigns` event', () => {
it('emits `delete` event on deleteButton `delete-selected-designs` event', () => {
createComponent();
return wrapper.vm.$nextTick().then(() => {
wrapper.find(DeleteButton).vm.$emit('deleteSelectedDesigns');
wrapper.find(DeleteButton).vm.$emit('delete-selected-designs');
expect(wrapper.emitted().delete).toBeTruthy();
});
});

View File

@ -19,7 +19,7 @@ exports[`Design management upload button component renders inverted upload desig
<input
accept="image/*"
class="hide"
class="gl-display-none"
multiple="multiple"
name="design_file"
type="file"
@ -44,7 +44,7 @@ exports[`Design management upload button component renders upload design button
<input
accept="image/*"
class="hide"
class="gl-display-none"
multiple="multiple"
name="design_file"
type="file"

View File

@ -97,7 +97,7 @@ describe('Design management index page', () => {
let moveDesignHandler;
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
const findSelectAllButton = () => wrapper.find('.js-select-all');
const findSelectAllButton = () => wrapper.find('[data-testid="select-all-designs-button"');
const findToolbar = () => wrapper.find('.qa-selector-toolbar');
const findDesignCollectionIsCopying = () =>
wrapper.find('[data-testid="design-collection-is-copying"');
@ -542,7 +542,9 @@ describe('Design management index page', () => {
await nextTick();
expect(findDeleteButton().exists()).toBe(true);
expect(findSelectAllButton().text()).toBe('Deselect all');
findDeleteButton().vm.$emit('deleteSelectedDesigns');
findDeleteButton().vm.$emit('delete-selected-designs');
const [{ variables }] = mutate.mock.calls[0];
expect(variables.filenames).toStrictEqual([mockDesigns[0].filename, mockDesigns[1].filename]);
});

View File

@ -35,8 +35,8 @@ describe('Pipelines Triggerer', () => {
wrapper.destroy();
});
it('should render a table cell', () => {
expect(wrapper.find('.table-section').exists()).toBe(true);
it('should render pipeline triggerer table cell', () => {
expect(wrapper.find('[data-testid="pipeline-triggerer"]').exists()).toBe(true);
});
it('should pass triggerer information when triggerer is provided', () => {

View File

@ -7,6 +7,7 @@ const projectPath = 'test/test';
describe('Pipeline Url Component', () => {
let wrapper;
const findTableCell = () => wrapper.find('[data-testid="pipeline-url-table-cell"]');
const findPipelineUrlLink = () => wrapper.find('[data-testid="pipeline-url-link"]');
const findScheduledTag = () => wrapper.find('[data-testid="pipeline-url-scheduled"]');
const findLatestTag = () => wrapper.find('[data-testid="pipeline-url-latest"]');
@ -43,10 +44,10 @@ describe('Pipeline Url Component', () => {
wrapper = null;
});
it('should render a table cell', () => {
it('should render pipeline url table cell', () => {
createComponent();
expect(wrapper.attributes('class')).toContain('table-section');
expect(findTableCell().exists()).toBe(true);
});
it('should render a link the provided path and id', () => {

View File

@ -1,4 +1,4 @@
import { GlFilteredSearch, GlButton, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { GlButton, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { chunk } from 'lodash';

View File

@ -1,7 +1,13 @@
import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesStatusBadge from '~/pipelines/components/pipelines_list/pipelines_status_badge.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
describe('Pipelines Table', () => {
let pipeline;
@ -29,7 +35,22 @@ describe('Pipelines Table', () => {
const findRows = () => wrapper.findAll('.commit.gl-responsive-table-row');
const findGlTable = () => wrapper.findComponent(GlTable);
const findLegacyTable = () => wrapper.findByTestId('ci-table');
const findStatusBadge = () => wrapper.findComponent(PipelinesStatusBadge);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findCommit = () => wrapper.findComponent(CommitComponent);
const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago);
const findActions = () => wrapper.findComponent(PipelineOperations);
const findLegacyTable = () => wrapper.findByTestId('legacy-ci-table');
const findTableRows = () => wrapper.findAll('[data-testid="pipeline-table-row"]');
const findStatusTh = () => wrapper.findByTestId('status-th');
const findPipelineTh = () => wrapper.findByTestId('pipeline-th');
const findTriggererTh = () => wrapper.findByTestId('triggerer-th');
const findCommitTh = () => wrapper.findByTestId('commit-th');
const findStagesTh = () => wrapper.findByTestId('stages-th');
const findTimeAgoTh = () => wrapper.findByTestId('timeago-th');
const findActionsTh = () => wrapper.findByTestId('actions-th');
preloadFixtures(jsonFixtureName);
@ -82,11 +103,82 @@ describe('Pipelines Table', () => {
});
describe('table with feature flag on', () => {
it('displays new table', () => {
createComponent(defaultProps, true);
beforeEach(() => {
createComponent({ pipelines: [pipeline], viewType: 'root' }, true);
});
it('displays new table', () => {
expect(findGlTable().exists()).toBe(true);
expect(findLegacyTable().exists()).toBe(false);
});
it('should render table head with correct columns', () => {
expect(findStatusTh().text()).toBe('Status');
expect(findPipelineTh().text()).toBe('Pipeline');
expect(findTriggererTh().text()).toBe('Triggerer');
expect(findCommitTh().text()).toBe('Commit');
expect(findStagesTh().text()).toBe('Stages');
expect(findTimeAgoTh().text()).toBe('Duration');
// last column should have no text in th
expect(findActionsTh().text()).toBe('');
});
it('should display a table row', () => {
expect(findTableRows()).toHaveLength(1);
});
describe('status cell', () => {
it('should render a status badge', () => {
expect(findStatusBadge().exists()).toBe(true);
});
it('should render status badge with correct path', () => {
expect(findStatusBadge().attributes('href')).toBe(pipeline.path);
});
});
describe('pipeline cell', () => {
it('should render pipeline information', () => {
expect(findPipelineInfo().exists()).toBe(true);
});
it('should display the pipeline id', () => {
expect(findPipelineInfo().text()).toContain(`#${pipeline.id}`);
});
});
describe('triggerer cell', () => {
it('should render the pipeline triggerer', () => {
expect(findTriggerer().exists()).toBe(true);
});
});
describe('commit cell', () => {
it('should render commit information', () => {
expect(findCommit().exists()).toBe(true);
});
it('should display and link to commit', () => {
expect(findCommit().text()).toContain(pipeline.commit.short_id);
expect(findCommit().props('commitUrl')).toBe(pipeline.commit.commit_path);
});
it('should display the commit author', () => {
expect(findCommit().props('author')).toEqual(pipeline.commit.author);
});
});
describe('duration cell', () => {
it('should render duration information', () => {
expect(findTimeAgo().exists()).toBe(true);
});
});
describe('operations cell', () => {
it('should render pipeline operations', () => {
expect(findActions().exists()).toBe(true);
});
});
});
});

View File

@ -7,7 +7,13 @@ import { trimText } from 'helpers/text_helper';
import { ENTER_KEY } from '~/lib/utils/keys';
import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants';
import {
X_TOTAL_HEADER,
DEFAULT_I18N,
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
} from '~/ref/constants';
import createStore from '~/ref/stores/';
const localVue = createLocalVue();
@ -26,6 +32,7 @@ describe('Ref selector component', () => {
let branchesApiCallSpy;
let tagsApiCallSpy;
let commitApiCallSpy;
let requestSpies;
const createComponent = (props = {}, attrs = {}) => {
wrapper = mount(RefSelector, {
@ -58,6 +65,7 @@ describe('Ref selector component', () => {
.mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]);
requestSpies = { branchesApiCallSpy, tagsApiCallSpy, commitApiCallSpy };
mock
.onGet(`/api/v4/projects/${projectId}/repository/branches`)
@ -592,4 +600,86 @@ describe('Ref selector component', () => {
});
});
});
describe('with non-default ref types', () => {
it.each`
enabledRefTypes | reqsCalled | reqsNotCalled
${[REF_TYPE_BRANCHES]} | ${['branchesApiCallSpy']} | ${['tagsApiCallSpy', 'commitApiCallSpy']}
${[REF_TYPE_TAGS]} | ${['tagsApiCallSpy']} | ${['branchesApiCallSpy', 'commitApiCallSpy']}
${[REF_TYPE_COMMITS]} | ${[]} | ${['branchesApiCallSpy', 'tagsApiCallSpy', 'commitApiCallSpy']}
${[REF_TYPE_TAGS, REF_TYPE_COMMITS]} | ${['tagsApiCallSpy']} | ${['branchesApiCallSpy', 'commitApiCallSpy']}
`(
'only calls $reqsCalled requests when $enabledRefTypes are enabled',
async ({ enabledRefTypes, reqsCalled, reqsNotCalled }) => {
createComponent({ enabledRefTypes });
await waitForRequests();
reqsCalled.forEach((req) => expect(requestSpies[req]).toHaveBeenCalledTimes(1));
reqsNotCalled.forEach((req) => expect(requestSpies[req]).not.toHaveBeenCalled());
},
);
it('only calls commitApiCallSpy when REF_TYPE_COMMITS is enabled', async () => {
createComponent({ enabledRefTypes: [REF_TYPE_COMMITS] });
updateQuery('abcd1234');
await waitForRequests();
expect(commitApiCallSpy).toHaveBeenCalledTimes(1);
expect(branchesApiCallSpy).not.toHaveBeenCalled();
expect(tagsApiCallSpy).not.toHaveBeenCalled();
});
it('triggers another search if enabled ref types change', async () => {
createComponent({ enabledRefTypes: [REF_TYPE_BRANCHES] });
await waitForRequests();
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
expect(tagsApiCallSpy).not.toHaveBeenCalled();
wrapper.setProps({
enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
});
await waitForRequests();
expect(branchesApiCallSpy).toHaveBeenCalledTimes(2);
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
});
it('if a ref type becomes disabled, its section is hidden, even if it had some results in store', async () => {
createComponent({ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] });
updateQuery('abcd1234');
await waitForRequests();
expect(findBranchesSection().exists()).toBe(true);
expect(findCommitsSection().exists()).toBe(true);
wrapper.setProps({ enabledRefTypes: [REF_TYPE_COMMITS] });
await waitForRequests();
expect(findBranchesSection().exists()).toBe(false);
expect(findCommitsSection().exists()).toBe(true);
});
it.each`
enabledRefType | findVisibleSection | findHiddenSections
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
${REF_TYPE_TAGS} | ${findTagsSection} | ${[findBranchesSection, findCommitsSection]}
${REF_TYPE_COMMITS} | ${findCommitsSection} | ${[findBranchesSection, findTagsSection]}
`(
'hides section headers if a single ref type is enabled',
async ({ enabledRefType, findVisibleSection, findHiddenSections }) => {
createComponent({ enabledRefTypes: [enabledRefType] });
updateQuery('abcd1234');
await waitForRequests();
expect(findVisibleSection().exists()).toBe(true);
expect(findVisibleSection().find('[data-testid="section-header"]').exists()).toBe(false);
findHiddenSections.forEach((findHiddenSection) =>
expect(findHiddenSection().exists()).toBe(false),
);
},
);
});
});

View File

@ -1,4 +1,5 @@
import testAction from 'helpers/vuex_action_helper';
import { ALL_REF_TYPES, REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS } from '~/ref/constants';
import * as actions from '~/ref/stores/actions';
import * as types from '~/ref/stores/mutation_types';
import createState from '~/ref/stores/state';
@ -25,6 +26,14 @@ describe('Ref selector Vuex store actions', () => {
state = createState();
});
describe('setEnabledRefTypes', () => {
it(`commits ${types.SET_ENABLED_REF_TYPES} with the enabled ref types`, () => {
testAction(actions.setProjectId, ALL_REF_TYPES, state, [
{ type: types.SET_PROJECT_ID, payload: ALL_REF_TYPES },
]);
});
});
describe('setProjectId', () => {
it(`commits ${types.SET_PROJECT_ID} with the new project ID`, () => {
const projectId = '4';
@ -46,12 +55,23 @@ describe('Ref selector Vuex store actions', () => {
describe('search', () => {
it(`commits ${types.SET_QUERY} with the new search query`, () => {
const query = 'hello';
testAction(actions.search, query, state, [{ type: types.SET_QUERY, payload: query }]);
});
it.each`
enabledRefTypes | expectedActions
${[REF_TYPE_BRANCHES]} | ${['searchBranches']}
${[REF_TYPE_COMMITS]} | ${['searchCommits']}
${[REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]} | ${['searchBranches', 'searchTags', 'searchCommits']}
`(`dispatches fetch actions for enabled ref types`, ({ enabledRefTypes, expectedActions }) => {
const query = 'hello';
state.enabledRefTypes = enabledRefTypes;
testAction(
actions.search,
query,
state,
[{ type: types.SET_QUERY, payload: query }],
[{ type: 'searchBranches' }, { type: 'searchTags' }, { type: 'searchCommits' }],
expectedActions.map((type) => ({ type })),
);
});
});

View File

@ -1,4 +1,4 @@
import { X_TOTAL_HEADER } from '~/ref/constants';
import { X_TOTAL_HEADER, ALL_REF_TYPES } from '~/ref/constants';
import * as types from '~/ref/stores/mutation_types';
import mutations from '~/ref/stores/mutations';
import createState from '~/ref/stores/state';
@ -13,6 +13,7 @@ describe('Ref selector Vuex store mutations', () => {
describe('initial state', () => {
it('is created with the correct structure and initial values', () => {
expect(state).toEqual({
enabledRefTypes: [],
projectId: null,
query: '',
@ -39,6 +40,14 @@ describe('Ref selector Vuex store mutations', () => {
});
});
describe(`${types.SET_ENABLED_REF_TYPES}`, () => {
it('sets the enabled ref types', () => {
mutations[types.SET_ENABLED_REF_TYPES](state, ALL_REF_TYPES);
expect(state.enabledRefTypes).toBe(ALL_REF_TYPES);
});
});
describe(`${types.SET_PROJECT_ID}`, () => {
it('updates the project ID', () => {
const newProjectId = '4';

View File

@ -29,6 +29,7 @@ RSpec.describe PreferencesHelper do
['Starred Projects', 'stars'],
["Your Projects' Activity", 'project_activity'],
["Starred Projects' Activity", 'starred_project_activity'],
["Followed Users' Activity", 'followed_user_activity'],
["Your Groups", 'groups'],
["Your To-Do List", 'todos'],
["Assigned Issues", 'issues'],

View File

@ -1025,4 +1025,75 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
end
end
end
describe 'applying pipeline variables' do
subject { seed_build }
let(:pipeline_variables) { [] }
let(:pipeline) do
build(:ci_empty_pipeline, project: project, sha: head_sha, variables: pipeline_variables)
end
context 'containing variable references' do
let(:pipeline_variables) do
[
build(:ci_pipeline_variable, key: 'A', value: '$B'),
build(:ci_pipeline_variable, key: 'B', value: '$C')
]
end
context 'when FF :variable_inside_variable is enabled' do
before do
stub_feature_flags(variable_inside_variable: [project])
end
it "does not have errors" do
expect(subject.errors).to be_empty
end
end
end
context 'containing cyclic reference' do
let(:pipeline_variables) do
[
build(:ci_pipeline_variable, key: 'A', value: '$B'),
build(:ci_pipeline_variable, key: 'B', value: '$C'),
build(:ci_pipeline_variable, key: 'C', value: '$A')
]
end
context 'when FF :variable_inside_variable is disabled' do
before do
stub_feature_flags(variable_inside_variable: false)
end
it "does not have errors" do
expect(subject.errors).to be_empty
end
end
context 'when FF :variable_inside_variable is enabled' do
before do
stub_feature_flags(variable_inside_variable: [project])
end
it "returns an error" do
expect(subject.errors).to contain_exactly(
'rspec: circular variable reference detected: ["A", "B", "C"]')
end
context 'with job:rules:[if:]' do
let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } }
it "included? does not raise" do
expect { subject.included? }.not_to raise_error
end
it "included? returns true" do
expect(subject.included?).to eq(true)
end
end
end
end
end
end

View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Cleanup::Redis::BatchDeleteByPattern, :clean_gitlab_redis_cache do
subject { described_class.new(patterns) }
describe 'execute' do
context 'when no patterns are passed' do
before do
expect(Gitlab::Redis::Cache).not_to receive(:with)
end
context 'with nil patterns' do
let(:patterns) { nil }
specify { expect { subject }.to raise_error(ArgumentError, 'Argument should be an Array of patterns') }
end
context 'with empty array patterns' do
let(:patterns) { [] }
specify { subject.execute }
end
end
context 'with patterns' do
context 'when key is not found' do
let(:patterns) { ['key'] }
before do
expect_any_instance_of(Redis).not_to receive(:del) # rubocop:disable RSpec/AnyInstanceOf
end
specify { subject.execute }
end
context 'with cache data' do
let(:cache_keys) { %w[key-test1 key-test2 key-test3 key-test4] }
before do
stub_const("#{described_class}::REDIS_CLEAR_BATCH_SIZE", 2)
write_to_cache
end
context 'with one key' do
let(:patterns) { ['key-test1'] }
it 'deletes the key' do
expect_any_instance_of(Redis).to receive(:del).with(patterns.first).once # rubocop:disable RSpec/AnyInstanceOf
subject.execute
end
end
context 'with many keys' do
let(:patterns) { %w[key-test1 key-test2] }
it 'deletes keys for each pattern separatelly' do
expect_any_instance_of(Redis).to receive(:del).with(patterns.first).once # rubocop:disable RSpec/AnyInstanceOf
expect_any_instance_of(Redis).to receive(:del).with(patterns.last).once # rubocop:disable RSpec/AnyInstanceOf
subject.execute
end
end
context 'with cache_keys over batch size' do
let(:patterns) { %w[key-test*] }
it 'deletes matched keys in batches' do
# redis scan returns the values in random order so just checking it is being called twice meaning
# scan returned results in 2 batches, which is what we expect
key_like = start_with('key-test')
expect_any_instance_of(Redis).to receive(:del).with(key_like, key_like).twice # rubocop:disable RSpec/AnyInstanceOf
subject.execute
end
end
end
end
end
end
def write_to_cache
Gitlab::Redis::Cache.with do |redis|
cache_keys.each_with_index do |cache_key, index|
redis.set(cache_key, index)
end
end
end

View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Cleanup::Redis::DescriptionTemplatesCacheKeysPatternBuilder, :clean_gitlab_redis_cache do
subject { described_class.new(project_ids).execute }
describe 'execute' do
context 'when build pattern for all description templates' do
RSpec.shared_examples 'all issue and merge request templates pattern' do
it 'builds pattern to remove all issue and merge request templates keys' do
expect(subject.count).to eq(2)
expect(subject).to match_array(%W[
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:issue_template_names_hash:*
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:merge_request_template_names_hash:*
])
end
end
context 'with project_ids == :all' do
let(:project_ids) { :all }
it_behaves_like 'all issue and merge request templates pattern'
end
end
context 'with project_ids' do
let_it_be(:project1) { create(:project, :repository) }
let_it_be(:project2) { create(:project, :repository) }
context 'with nil project_ids' do
let(:project_ids) { nil }
specify { expect { subject }.to raise_error(ArgumentError, 'project_ids can either be an array of project IDs or :all') }
end
context 'with project_ids as string' do
let(:project_ids) { '1' }
specify { expect { subject }.to raise_error(ArgumentError, 'project_ids can either be an array of project IDs or :all') }
end
context 'with invalid project_ids as array of strings' do
let(:project_ids) { %w[a b] }
specify { expect { subject }.to raise_error(ArgumentError, 'Invalid Project ID. Please ensure all passed in project ids values are valid integer project ids.') }
end
context 'with non existent project id' do
let(:project_ids) { [non_existing_record_id] }
it 'no patterns are built' do
expect(subject.count).to eq(0)
end
end
context 'with one project_id' do
let(:project_ids) { [project1.id] }
it 'builds patterns for the project' do
expect(subject.count).to eq(2)
expect(subject).to match_array(%W[
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:issue_template_names_hash:#{project1.full_path}:#{project1.id}
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:merge_request_template_names_hash:#{project1.full_path}:#{project1.id}
])
end
end
context 'with many project_ids' do
let(:project_ids) { [project1.id, project2.id] }
RSpec.shared_examples 'builds patterns for the given projects' do
it 'builds patterns for the given projects' do
expect(subject.count).to eq(4)
expect(subject).to match_array(%W[
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:issue_template_names_hash:#{project1.full_path}:#{project1.id}
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:merge_request_template_names_hash:#{project1.full_path}:#{project1.id}
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:issue_template_names_hash:#{project2.full_path}:#{project2.id}
#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:merge_request_template_names_hash:#{project2.full_path}:#{project2.id}
])
end
end
it_behaves_like 'builds patterns for the given projects'
context 'with project_ids as string' do
let(:project_ids) { [project1.id.to_s, project2.id.to_s] }
it_behaves_like 'builds patterns for the given projects'
end
end
end
end
end

View File

@ -797,6 +797,50 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
end
describe 'setting the application context' do
subject { request_job }
context 'when triggered by a user' do
let(:job) { create(:ci_build, user: user, project: project) }
subject { request_job(id: job.id) }
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { user: user.username, project: project.full_path } }
end
it_behaves_like 'not executing any extra queries for the application context', 3 do
# Extra queries: User, Project, Route
let(:subject_proc) { proc { request_job(id: job.id) } }
end
end
context 'when the runner is of project type' do
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { project: project.full_path } }
end
it_behaves_like 'not executing any extra queries for the application context', 2 do
# Extra queries: Project, Route
let(:subject_proc) { proc { request_job } }
end
end
context 'when the runner is of group type' do
let(:group) { create(:group) }
let(:runner) { create(:ci_runner, :group, groups: [group]) }
it_behaves_like 'storing arguments in the application context' do
let(:expected_params) { { root_namespace: group.full_path_components.first } }
end
it_behaves_like 'not executing any extra queries for the application context', 2 do
# Extra queries: Group, Route
let(:subject_proc) { proc { request_job } }
end
end
end
def request_job(token = runner.token, **params)
new_params = params.merge(token: token, last_update: last_update)
post api('/jobs/request'), params: new_params.to_json, headers: { 'User-Agent' => user_agent, 'Content-Type': 'application/json' }

View File

@ -35,6 +35,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
end
context 'when valid token is provided' do
def request
post api('/runners'), params: { token: token }
end
it 'creates runner with default values' do
post api('/runners'), params: { token: registration_token }
@ -51,9 +55,10 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
context 'when project token is used' do
let(:project) { create(:project) }
let(:token) { project.runners_token }
it 'creates project runner' do
post api('/runners'), params: { token: project.runners_token }
request
expect(response).to have_gitlab_http_status(:created)
expect(project.runners.size).to eq(1)
@ -62,13 +67,24 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(runner.token).not_to eq(project.runners_token)
expect(runner).to be_project_type
end
it_behaves_like 'storing arguments in the application context' do
subject { request }
let(:expected_params) { { project: project.full_path } }
end
it_behaves_like 'not executing any extra queries for the application context' do
let(:subject_proc) { proc { request } }
end
end
context 'when group token is used' do
let(:group) { create(:group) }
let(:token) { group.runners_token }
it 'creates a group runner' do
post api('/runners'), params: { token: group.runners_token }
request
expect(response).to have_gitlab_http_status(:created)
expect(group.runners.reload.size).to eq(1)
@ -77,6 +93,16 @@ RSpec.describe API::Ci::Runner, :clean_gitlab_redis_shared_state do
expect(runner.token).not_to eq(group.runners_token)
expect(runner).to be_group_type
end
it_behaves_like 'storing arguments in the application context' do
subject { request }
let(:expected_params) { { root_namespace: group.full_path_components.first } }
end
it_behaves_like 'not executing any extra queries for the application context' do
let(:subject_proc) { proc { request } }
end
end
end

View File

@ -172,8 +172,13 @@ module TestEnv
Gitlab::SetupHelper::Gitaly.create_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
Gitlab::SetupHelper::Gitaly.create_configuration(
gitaly_dir,
{ 'default' => repos_path }, force: true,
options: { gitaly_socket: "gitaly2.socket", config_filename: "gitaly2.config.toml" }
{ 'default' => repos_path },
force: true,
options: {
internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"),
gitaly_socket: "gitaly2.socket",
config_filename: "gitaly2.config.toml"
}
)
Gitlab::SetupHelper::Praefect.create_configuration(gitaly_dir, { 'praefect' => repos_path }, force: true)
end

View File

@ -22,3 +22,19 @@ RSpec.shared_examples 'storing arguments in the application context' do
hash.transform_keys! { |key| "meta.#{key}" }
end
end
RSpec.shared_examples 'not executing any extra queries for the application context' do |expected_extra_queries = 0|
it 'does not execute more queries than without adding anything to the application context' do
# Call the subject once to memoize all factories being used for the spec, so they won't
# add any queries to the expectation.
subject_proc.call
expect do
allow(Gitlab::ApplicationContext).to receive(:push).and_call_original
subject_proc.call
end.to issue_same_number_of_queries_as {
allow(Gitlab::ApplicationContext).to receive(:push)
subject_proc.call
}.with_threshold(expected_extra_queries).ignoring_cached_queries
end
end

View File

@ -2,7 +2,7 @@
require 'rake_helper'
RSpec.describe 'clearing redis cache' do
RSpec.describe 'clearing redis cache', :clean_gitlab_redis_cache do
before do
Rake.application.rake_require 'tasks/cache'
end
@ -21,4 +21,27 @@ RSpec.describe 'clearing redis cache' do
expect { run_rake_task('cache:clear:redis') }.to change { pipeline_status.has_cache? }
end
end
describe 'invoking clear description templates cache rake task' do
using RSpec::Parameterized::TableSyntax
before do
stub_env('project_ids', project_ids) if project_ids
service = double(:service, execute: true)
expect(Gitlab::Cleanup::Redis::DescriptionTemplatesCacheKeysPatternBuilder).to receive(:new).with(expected_project_ids).and_return(service)
expect(Gitlab::Cleanup::Redis::BatchDeleteByPattern).to receive(:new).and_return(service)
end
where(:project_ids, :expected_project_ids) do
nil | [] # this acts as no argument is being passed
'1' | %w[1]
'1, 2, 3' | %w[1 2 3]
'1, 2, some-string, 3' | %w[1 2 some-string 3]
end
with_them do
specify { run_rake_task('cache:clear:description_templates') }
end
end
end