Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-03 18:08:29 +00:00
parent 0e13b2c715
commit 592223823c
130 changed files with 1108 additions and 6726 deletions

View File

@ -296,6 +296,21 @@ gitlab:setup:
paths:
- log/*.log
db:backup_and_restore:
extends: .db-job-base
variables:
SETUP_DB: "false"
GITLAB_ASSUME_YES: "1"
script:
- . scripts/prepare_build.sh
- bundle exec rake db:drop db:create db:structure:load db:seed_fu
- mkdir -p tmp/tests/public/uploads tmp/tests/{artifacts,pages,lfs-objects,registry}
- bundle exec rake gitlab:backup:create
- date
- bundle exec rake gitlab:backup:restore
rules:
- changes: ["lib/backup/**/*"]
rspec:coverage:
extends:
- .rails-job-base

View File

@ -638,13 +638,6 @@ Style/RedundantInterpolation:
Style/RedundantSelf:
Enabled: false
# Offense count: 2
# Cop supports --auto-correct.
Style/RedundantSort:
Exclude:
- 'app/presenters/packages/nuget/search_results_presenter.rb'
- 'spec/presenters/packages/nuget/search_results_presenter_spec.rb'
# Offense count: 120
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
@ -697,11 +690,6 @@ Rails/SaveBang:
- 'ee/spec/initializers/fog_google_https_private_urls_spec.rb'
- 'ee/spec/lib/analytics/merge_request_metrics_calculator_spec.rb'
- 'ee/spec/lib/ee/gitlab/auth/ldap/sync/group_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/move_epic_issues_after_epics_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/populate_any_approval_rule_for_merge_requests_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/populate_any_approval_rule_for_projects_spec.rb'
- 'ee/spec/lib/ee/gitlab/background_migration/prune_orphaned_geo_events_spec.rb'
- 'ee/spec/lib/ee/gitlab/checks/push_rules/commit_check_spec.rb'
- 'ee/spec/lib/ee/gitlab/ci/pipeline/quota/activity_spec.rb'
- 'ee/spec/lib/gitlab/auth/ldap/access_spec.rb'
@ -829,10 +817,6 @@ Rails/SaveBang:
- 'ee/spec/services/merge_requests/remove_approval_service_spec.rb'
- 'ee/spec/services/merge_requests/update_blocks_service_spec.rb'
- 'ee/spec/services/merge_trains/refresh_merge_request_service_spec.rb'
- 'ee/spec/services/projects/after_rename_service_spec.rb'
- 'ee/spec/services/projects/import_export/export_service_spec.rb'
- 'ee/spec/services/projects/update_mirror_service_spec.rb'
- 'ee/spec/services/projects/update_service_spec.rb'
- 'ee/spec/services/quick_actions/interpret_service_spec.rb'
- 'ee/spec/services/slash_commands/global_slack_handler_spec.rb'
- 'ee/spec/services/start_pull_mirroring_service_spec.rb'
@ -1111,9 +1095,6 @@ Rails/SaveBang:
- 'spec/models/concerns/subscribable_spec.rb'
- 'spec/models/concerns/token_authenticatable_spec.rb'
- 'spec/models/container_repository_spec.rb'
- 'spec/models/cycle_analytics/issue_spec.rb'
- 'spec/models/cycle_analytics/plan_spec.rb'
- 'spec/models/cycle_analytics/production_spec.rb'
- 'spec/models/deploy_keys_project_spec.rb'
- 'spec/models/deploy_token_spec.rb'
- 'spec/models/deployment_spec.rb'
@ -1172,9 +1153,6 @@ Rails/SaveBang:
- 'spec/presenters/ci/build_runner_presenter_spec.rb'
- 'spec/presenters/ci/trigger_presenter_spec.rb'
- 'spec/presenters/packages/conan/package_presenter_spec.rb'
- 'spec/requests/api/access_requests_spec.rb'
- 'spec/requests/api/boards_spec.rb'
- 'spec/requests/api/branches_spec.rb'
- 'spec/requests/api/ci/runner_spec.rb'
- 'spec/requests/api/commit_statuses_spec.rb'
- 'spec/requests/api/conan_packages_spec.rb'
@ -1196,9 +1174,6 @@ Rails/SaveBang:
- 'spec/requests/api/merge_request_diffs_spec.rb'
- 'spec/requests/api/merge_requests_spec.rb'
- 'spec/requests/api/notes_spec.rb'
- 'spec/requests/api/pages/internal_access_spec.rb'
- 'spec/requests/api/pages/private_access_spec.rb'
- 'spec/requests/api/pages/public_access_spec.rb'
- 'spec/requests/api/pipeline_schedules_spec.rb'
- 'spec/requests/api/project_import_spec.rb'
- 'spec/requests/api/project_milestones_spec.rb'
@ -1208,11 +1183,6 @@ Rails/SaveBang:
- 'spec/requests/lfs_http_spec.rb'
- 'spec/requests/profiles/notifications_controller_spec.rb'
- 'spec/requests/projects/cycle_analytics_events_spec.rb'
- 'spec/serializers/environment_status_entity_spec.rb'
- 'spec/serializers/issue_entity_spec.rb'
- 'spec/serializers/job_entity_spec.rb'
- 'spec/serializers/merge_request_poll_widget_entity_spec.rb'
- 'spec/serializers/merge_request_widget_entity_spec.rb'
- 'spec/services/auth/container_registry_authentication_service_spec.rb'
- 'spec/services/auto_merge/base_service_spec.rb'
- 'spec/services/auto_merge_service_spec.rb'
@ -1232,11 +1202,6 @@ Rails/SaveBang:
- 'spec/services/issuable/bulk_update_service_spec.rb'
- 'spec/services/issuable/clone/attributes_rewriter_spec.rb'
- 'spec/services/issuable/common_system_notes_service_spec.rb'
- 'spec/services/issues/close_service_spec.rb'
- 'spec/services/issues/create_service_spec.rb'
- 'spec/services/issues/export_csv_service_spec.rb'
- 'spec/services/issues/reopen_service_spec.rb'
- 'spec/services/issues/update_service_spec.rb'
- 'spec/services/labels/promote_service_spec.rb'
- 'spec/services/members/destroy_service_spec.rb'
- 'spec/services/merge_requests/build_service_spec.rb'

View File

@ -1 +1 @@
8.44.0
8.45.0

View File

@ -28,7 +28,8 @@ export default class AjaxLoadingSpinner {
static toggleLoadingIcon(iconElement) {
const { classList } = iconElement;
classList.toggle(iconElement.dataset.icon);
classList.toggle('fa-spinner');
classList.toggle('fa-spin');
classList.toggle('gl-spinner');
classList.toggle('gl-spinner-orange');
classList.toggle('gl-spinner-sm');
}
}

View File

@ -29,6 +29,10 @@ export default {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
scopedIssueBoardFeatureEnabled: {
type: Boolean,
required: false,
@ -198,6 +202,7 @@ export default {
:board="board"
:can-admin-board="canAdminBoard"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:enable-scoped-labels="enableScopedLabels"
:project-id="projectId"
:group-id="groupId"

View File

@ -61,6 +61,10 @@ export default {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
projectId: {
type: Number,
required: true,
@ -332,6 +336,7 @@ export default {
<board-form
v-if="currentPage"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:project-id="projectId"
:group-id="groupId"
:can-admin-board="canAdminBoard"

View File

@ -1,8 +1,7 @@
<script>
/* eslint-disable vue/no-v-html */
import { mapActions, mapGetters, mapState } from 'vuex';
import { escape } from 'lodash';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { sprintf, __ } from '../../../locale';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Tabs from '../../../vue_shared/components/tabs/tabs';
@ -22,6 +21,9 @@ export default {
EmptyState,
GlLoadingIcon,
},
directives: {
SafeHtml,
},
computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
...mapGetters(['currentProject']),
@ -84,7 +86,7 @@ export default {
<div v-else-if="latestPipeline.yamlError" class="bs-callout bs-callout-danger">
<p class="gl-mb-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p>
<p class="gl-mb-0 break-word">{{ latestPipeline.yamlError }}</p>
<p class="gl-mb-0" v-html="ciLintText"></p>
<p v-safe-html="ciLintText" class="gl-mb-0"></p>
</div>
<tabs v-else class="ide-pipeline-list">
<tab :active="!pipelineFailed">

View File

@ -20,7 +20,7 @@ export default {
return h(
'span',
{
class: ['ws-pre-wrap', content.style],
class: ['gl-white-space-pre-wrap', content.style],
},
content.text,
);

View File

@ -52,7 +52,7 @@ export default {
<span
v-for="(content, i) in line.content"
:key="i"
class="line-text w-100 ws-pre-wrap"
class="line-text w-100 gl-white-space-pre-wrap"
:class="content.style"
>{{ content.text }}</span
>

View File

@ -13,6 +13,8 @@ import { __, sprintf } from '~/locale';
import Api from '~/api';
import { deprecatedCreateFlash as createFlash } from '~/flash';
const SEARCH_DEBOUNCE_MS = 250;
export default {
components: {
GlNewDropdown,
@ -95,12 +97,15 @@ export default {
// lodash attaches to the function, which is
// made inaccessible by Vue. More info:
// https://stackoverflow.com/a/52988020/1063392
this.debouncedSearchMilestones = debounce(this.searchMilestones, 100);
this.debouncedSearchMilestones = debounce(this.searchMilestones, SEARCH_DEBOUNCE_MS);
},
mounted() {
this.fetchMilestones();
},
methods: {
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
fetchMilestones() {
this.requestCount += 1;
@ -183,7 +188,7 @@ export default {
</script>
<template>
<gl-new-dropdown>
<gl-new-dropdown v-bind="$attrs" class="project-milestone-combobox" @shown="focusSearchBox">
<template slot="button-content">
<span ref="buttonText" class="flex-grow-1 ml-1 text-muted">{{
selectedMilestonesLabel
@ -198,6 +203,7 @@ export default {
<gl-new-dropdown-divider />
<gl-search-box-by-type
ref="searchBox"
v-model.trim="searchQuery"
class="gl-m-3"
:placeholder="this.$options.translations.searchMilestones"

View File

@ -1,9 +1,12 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { __, sprintf } from '~/locale';
export default {
directives: {
SafeHtml,
},
computed: {
...mapGetters(['getNotesDataByProp']),
registerLink() {
@ -31,5 +34,5 @@ export default {
</script>
<template>
<div class="disabled-comment text-center" v-html="signedOutText"></div>
<div v-safe-html="signedOutText" class="disabled-comment text-center"></div>
</template>

View File

@ -1,5 +1,5 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { escape } from 'lodash';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { s__, sprintf } from '~/locale';
@ -8,6 +8,9 @@ export default {
components: {
DeprecatedModal,
},
directives: {
SafeHtml,
},
props: {
deleteProjectUrl: {
type: String,
@ -94,8 +97,8 @@ export default {
@cancel="onCancel"
>
<template #body="props">
<p v-html="props.text"></p>
<p v-html="confirmationTextLabel"></p>
<p v-safe-html="props.text"></p>
<p v-safe-html="confirmationTextLabel"></p>
<form ref="form" :action="deleteProjectUrl" method="post">
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />

View File

@ -1,11 +1,9 @@
<script>
import { GlButton } from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
export default {
name: 'PipelineNavControls',
components: {
LoadingButton,
GlButton,
},
props: {
@ -52,13 +50,14 @@ export default {
{{ s__('Pipelines|Run Pipeline') }}
</gl-button>
<loading-button
<gl-button
v-if="resetCachePath"
:loading="isResetCacheButtonLoading"
:label="s__('Pipelines|Clear Runner Caches')"
class="js-clear-cache"
@click="onClickResetCache"
/>
>
{{ s__('Pipelines|Clear Runner Caches') }}
</gl-button>
<gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint">
{{ s__('Pipelines|CI Lint') }}

View File

@ -86,7 +86,7 @@ export default {
<slot name="modal-body"></slot>
<p class="gl-mb-1">{{ $options.strings.confirmText }}</p>
<p>
<code class="ws-pre-wrap">{{ confirmPhrase }}</code>
<code class="gl-white-space-pre-wrap">{{ confirmPhrase }}</code>
</p>
<gl-form-input
id="confirm_name_input"

View File

@ -6,19 +6,21 @@ import { __, sprintf } from '~/locale';
import TitleField from '~/vue_shared/components/form/title.vue';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
import UpdateSnippetMutation from '../mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '../mutations/createSnippet.mutation.graphql';
import { getSnippetMixin } from '../mixins/snippets';
import {
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_CREATE_MUTATION_ERROR,
SNIPPET_UPDATE_MUTATION_ERROR,
SNIPPET_VISIBILITY_PRIVATE,
} from '../constants';
import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
import SnippetBlobActionsEdit from './snippet_blob_actions_edit.vue';
import SnippetVisibilityEdit from './snippet_visibility_edit.vue';
import SnippetDescriptionEdit from './snippet_description_edit.vue';
import { SNIPPET_MARK_EDIT_APP_START } from '~/performance_constants';
export default {
components: {
@ -31,6 +33,15 @@ export default {
GlLoadingIcon,
},
mixins: [getSnippetMixin],
apollo: {
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { selectedLevel } }) {
this.selectedLevelDefault = selectedLevel;
},
},
},
props: {
markdownPreviewPath: {
type: String,
@ -56,6 +67,7 @@ export default {
isUpdating: false,
newSnippet: false,
actions: [],
selectedLevelDefault: SNIPPET_VISIBILITY_PRIVATE,
};
},
computed: {
@ -98,6 +110,13 @@ export default {
descriptionFieldId() {
return `${this.isProjectSnippet ? 'project' : 'personal'}_snippet_description`;
},
newSnippetSchema() {
return {
title: '',
description: '',
visibilityLevel: this.selectedLevelDefault,
};
},
},
beforeCreate() {
performance.mark(SNIPPET_MARK_EDIT_APP_START);
@ -126,7 +145,7 @@ export default {
},
onNewSnippetFetched() {
this.newSnippet = true;
this.snippet = this.$options.newSnippetSchema;
this.snippet = this.newSnippetSchema;
},
onExistingSnippetFetched() {
this.newSnippet = false;
@ -184,11 +203,6 @@ export default {
this.actions = actions;
},
},
newSnippetSchema: {
title: '',
description: '',
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
},
};
</script>
<template>

View File

@ -1,11 +1,8 @@
<script>
import { GlIcon, GlFormGroup, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
import defaultVisibilityQuery from '../queries/snippet_visibility.query.graphql';
import { defaultSnippetVisibilityLevels } from '../utils/blob';
import { SNIPPET_LEVELS_RESTRICTED, SNIPPET_LEVELS_DISABLED } from '~/snippets/constants';
export default {
components: {
@ -15,6 +12,16 @@ export default {
GlFormRadioGroup,
GlLink,
},
apollo: {
defaultVisibility: {
query: defaultVisibilityQuery,
manual: true,
result({ data: { visibilityLevels, multipleLevelsRestricted } }) {
this.visibilityLevels = defaultSnippetVisibilityLevels(visibilityLevels);
this.multipleLevelsRestricted = multipleLevelsRestricted;
},
},
},
props: {
helpLink: {
type: String,
@ -28,19 +35,17 @@ export default {
},
value: {
type: String,
required: false,
default: SNIPPET_VISIBILITY_PRIVATE,
required: true,
},
},
computed: {
visibilityOptions() {
return [
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
].map(key => ({ value: key, ...SNIPPET_VISIBILITY[key] }));
},
data() {
return {
visibilityLevels: [],
multipleLevelsRestricted: false,
};
},
SNIPPET_LEVELS_DISABLED,
SNIPPET_LEVELS_RESTRICTED,
};
</script>
<template>
@ -51,10 +56,10 @@ export default {
><gl-icon :size="12" name="question"
/></gl-link>
</label>
<gl-form-group id="visibility-level-setting">
<gl-form-radio-group v-bind="$attrs" :checked="value" stacked v-on="$listeners">
<gl-form-group id="visibility-level-setting" class="gl-mb-0">
<gl-form-radio-group :checked="value" stacked v-bind="$attrs" v-on="$listeners">
<gl-form-radio
v-for="option in visibilityOptions"
v-for="option in visibilityLevels"
:key="option.value"
:value="option.value"
class="mb-3"
@ -71,5 +76,12 @@ export default {
</gl-form-radio>
</gl-form-radio-group>
</gl-form-group>
<div class="text-muted" data-testid="restricted-levels-info">
<template v-if="!visibilityLevels.length">{{ $options.SNIPPET_LEVELS_DISABLED }}</template>
<template v-else-if="multipleLevelsRestricted">{{
$options.SNIPPET_LEVELS_RESTRICTED
}}</template>
</div>
</div>
</template>

View File

@ -33,3 +33,15 @@ export const SNIPPET_BLOB_ACTION_MOVE = 'move';
export const SNIPPET_BLOB_ACTION_DELETE = 'delete';
export const SNIPPET_MAX_BLOBS = 10;
export const SNIPPET_LEVELS_MAP = {
0: SNIPPET_VISIBILITY_PRIVATE,
10: SNIPPET_VISIBILITY_INTERNAL,
20: SNIPPET_VISIBILITY_PUBLIC,
};
export const SNIPPET_LEVELS_RESTRICTED = __(
'Other visibility settings have been disabled by the administrator.',
);
export const SNIPPET_LEVELS_DISABLED = __(
'Visibility settings have been disabled by the administrator.',
);

View File

@ -5,6 +5,7 @@ import createDefaultClient from '~/lib/graphql';
import SnippetsShow from './components/show.vue';
import SnippetsEdit from './components/edit.vue';
import { SNIPPET_LEVELS_MAP, SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
Vue.use(VueApollo);
Vue.use(Translate);
@ -18,13 +19,23 @@ function appFactory(el, Component) {
defaultClient: createDefaultClient(),
});
const { visibilityLevels, selectedLevel, multipleLevelsRestricted, ...restDataset } = el.dataset;
apolloProvider.clients.defaultClient.cache.writeData({
data: {
visibilityLevels: JSON.parse(visibilityLevels),
selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
},
});
return new Vue({
el,
apolloProvider,
render(createElement) {
return createElement(Component, {
props: {
...el.dataset,
...restDataset,
},
});
},

View File

@ -0,0 +1,5 @@
query defaultSnippetVisibility {
visibilityLevels @client
selectedLevel @client
multipleLevelsRestricted @client
}

View File

@ -4,6 +4,8 @@ import {
SNIPPET_BLOB_ACTION_UPDATE,
SNIPPET_BLOB_ACTION_MOVE,
SNIPPET_BLOB_ACTION_DELETE,
SNIPPET_LEVELS_MAP,
SNIPPET_VISIBILITY,
} from '../constants';
const createLocalId = () => uniqueId('blob_local_');
@ -64,3 +66,16 @@ export const diffAll = (blobs, origBlobs) => {
return [...deletedEntries, ...newEntries];
};
export const defaultSnippetVisibilityLevels = arr => {
if (Array.isArray(arr)) {
return arr.map(l => {
const translatedLevel = SNIPPET_LEVELS_MAP[l];
return {
value: translatedLevel,
...SNIPPET_VISIBILITY[translatedLevel],
};
});
}
return [];
};

View File

@ -1,11 +1,14 @@
<script>
/* eslint-disable vue/no-v-html */
import Vue from 'vue';
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
directives: {
SafeHtml,
},
props: {
lineType: {
type: String,
@ -116,6 +119,6 @@ export default {
<template>
<div>
<div class="flash-container js-suggestions-flash"></div>
<div v-show="isRendered" ref="container" class="md" v-html="noteHtml"></div>
<div v-show="isRendered" ref="container" v-safe-html="noteHtml" class="md"></div>
</div>
</template>

View File

@ -14,7 +14,10 @@ import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue';
import DropdownCreateLabel from './dropdown_create_label.vue';
import { DropdownVariant } from '../labels_select_vue/constants';
export default {
DropdownVariant,
components: {
DropdownTitle,
DropdownValue,
@ -80,6 +83,11 @@ export default {
required: false,
default: false,
},
variant: {
type: String,
required: false,
default: DropdownVariant.Sidebar,
},
},
computed: {
hiddenInputName() {
@ -123,7 +131,7 @@ export default {
<template>
<div class="block labels js-labels-block">
<dropdown-value-collapsed
v-if="showCreate"
v-if="showCreate && variant === $options.DropdownVariant.Sidebar"
:labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/>
@ -150,18 +158,21 @@ export default {
:labels-path="labelsPath"
:namespace="namespace"
:labels="context.labels"
:show-extra-options="!showCreate"
:show-extra-options="!showCreate || variant !== $options.DropdownVariant.Sidebar"
:enable-scoped-labels="enableScopedLabels"
/>
<div
class="dropdown-menu dropdown-select dropdown-menu-paging
dropdown-menu-labels dropdown-menu-selectable"
class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"
>
<div class="dropdown-page-one">
<dropdown-header v-if="showCreate" />
<dropdown-header v-if="showCreate && variant === $options.DropdownVariant.Sidebar" />
<dropdown-search-input />
<div class="dropdown-content" data-qa-selector="labels_dropdown_content"></div>
<div class="dropdown-loading"><gl-loading-icon /></div>
<div class="dropdown-loading">
<gl-loading-icon
class="gl-display-flex gl-justify-content-center gl-align-items-center gl-h-full"
/>
</div>
<dropdown-footer
v-if="showCreate"
:labels-web-url="labelsWebUrl"

View File

@ -1,7 +1,11 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
headerTitle: {
type: String,
@ -10,7 +14,11 @@ export default {
},
},
created() {
this.suggestedColors = gon.suggested_label_colors;
const rawLabelsColors = gon.suggested_label_colors;
this.suggestedColors = Object.keys(rawLabelsColors).map(colorCode => ({
colorCode,
title: rawLabelsColors[colorCode],
}));
},
};
</script>
@ -46,10 +54,12 @@ export default {
<a
v-for="(color, index) in suggestedColors"
:key="index"
:data-color="color"
v-gl-tooltip
:data-color="color.colorCode"
:style="{
backgroundColor: color,
backgroundColor: color.colorCode,
}"
:title="color.title"
href="#"
>
&nbsp;

View File

@ -416,7 +416,6 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; }
.ws-initial { white-space: initial; }
.ws-normal { white-space: normal; }
.ws-pre-wrap { white-space: pre-wrap; }
.overflow-auto { overflow: auto; }
.overflow-visible { overflow: visible; }

View File

@ -171,6 +171,13 @@
}
}
.labels {
// Prevent double scroll-bars for labels dropdown.
.dropdown-menu-toggle.wide + .dropdown-select {
max-height: unset;
}
}
.gl-dropdown .dropdown-menu-toggle {
padding-right: $gl-padding-8;

View File

@ -878,7 +878,11 @@ module Ci
end
def has_coverage_reports?
self.has_reports?(Ci::JobArtifact.coverage_reports)
pipeline_artifacts&.has_code_coverage?
end
def can_generate_coverage_reports?
has_reports?(Ci::JobArtifact.coverage_reports)
end
def test_report_summary

View File

@ -1372,7 +1372,7 @@ class MergeRequest < ApplicationRecord
def has_coverage_reports?
return false unless Feature.enabled?(:coverage_report_view, project)
actual_head_pipeline&.pipeline_artifacts&.has_code_coverage?
actual_head_pipeline&.has_coverage_reports?
end
def has_terraform_reports?

View File

@ -6,6 +6,7 @@ class Packages::Pypi::Metadatum < ApplicationRecord
belongs_to :package, -> { where(package_type: :pypi) }, inverse_of: :pypi_metadatum
validates :package, presence: true
validates :required_python, length: { maximum: 50 }, allow_blank: true
validate :pypi_package_type

View File

@ -49,7 +49,7 @@ module Packages
def latest_version(packages)
versions = packages.map(&:version).compact
VersionSorter.sort(versions).last # rubocop: disable Style/UnneededSort
VersionSorter.sort(versions).last # rubocop: disable Style/RedundantSort
end
end
end

View File

@ -4,8 +4,8 @@ module Ci
class CreateArtifactService
def execute(pipeline)
return unless ::Gitlab::Ci::Features.coverage_report_view?(pipeline.project)
return unless pipeline.has_coverage_reports?
return if pipeline.pipeline_artifacts.has_code_coverage?
return unless pipeline.can_generate_coverage_reports?
return if pipeline.has_coverage_reports?
file = build_carrierwave_file(pipeline)

View File

@ -7,11 +7,17 @@ module Packages
def execute
::Packages::Package.transaction do
Packages::Pypi::Metadatum.upsert(
package_id: created_package.id,
meta = Packages::Pypi::Metadatum.new(
package: created_package,
required_python: params[:requires_python]
)
unless meta.valid?
raise ActiveRecord::RecordInvalid.new(meta)
end
Packages::Pypi::Metadatum.upsert(meta.attributes)
::Packages::CreatePackageFileService.new(created_package, file_params).execute
end
end

View File

@ -1,4 +1,4 @@
- page_title _("Packages")
- page_title _("Package Registry")
.row
.col-12

View File

@ -1,4 +1,4 @@
- page_title _("Packages")
- page_title _("Package Registry")
.row
.col-12

View File

@ -1,7 +1,7 @@
- add_to_breadcrumbs _("Packages"), project_packages_path(@project)
- add_to_breadcrumbs _("Package Registry"), project_packages_path(@project)
- add_to_breadcrumbs @package.name, project_packages_path(@project)
- breadcrumb_title @package.version
- page_title _("Packages")
- page_title _("Package Registry")
.row
.col-12

View File

@ -10,6 +10,7 @@
can_admin_board: can?(current_user, :admin_board, parent).to_s,
multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s,
labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: true),
labels_web_url: parent.is_a?(Project) ? project_labels_path(@project) : group_labels_path(@group),
project_id: @project&.id,
group_id: @group&.id,
scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false',

View File

@ -1,5 +1,6 @@
- if Feature.enabled?(:snippets_edit_vue)
#js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access") } }
- available_visibility_levels = available_visibility_levels(@snippet)
#js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access"), 'visibility_levels': available_visibility_levels, 'selected_level': snippets_selected_visibility_level(available_visibility_levels, @snippet.visibility_level), 'multiple_levels_restricted': multiple_visibility_levels_restricted? } }
- else
.snippet-form-holder
= form_for @snippet, url: url,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
---
title: Migrate '.fa-spinner' to '.spinner' for 'app/assets/javascripts/ajax_loading_spinner.js'
merge_request: 41147
author: Gilang Gumilar
type: changed

View File

@ -0,0 +1,5 @@
---
title: Adjust the Package Registry breadcrumb to match navigation
merge_request: 41264
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Validates pypi required_python size to avoid 500 error
merge_request: 40803
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Replace v-html with v-safe-html in note_signed_out_widget_spec.js
merge_request: 41219
author: Kev @KevSlashNull
type: other

View File

@ -0,0 +1,5 @@
---
title: Replace v-html with v-safe-html in delete_project_modal.vue
merge_request: 41130
author: Kev @KevSlashNull
type: other

View File

@ -0,0 +1,5 @@
---
title: Replace v-html with v-safe-html in list.vue
merge_request: 41145
author: Kev @KevSlashNull
type: other

View File

@ -0,0 +1,5 @@
---
title: Replace v-html with v-safe-html in suggestions.vue
merge_request: 41200
author: Kev @KevSlashNull
type: other

View File

@ -0,0 +1,5 @@
---
title: Update Workhorse to v8.45.0
merge_request: 41293
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix create & manage label actions in Labels dropdown
merge_request: 40511
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for spec/serializers/*
merge_request: 41309
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for spec/services/issues/*
merge_request: 41312
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for spec/requests/api/pages/*
merge_request: 41324
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for spec/models/cycle_analytics/*
merge_request: 41326
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for ee/spec/services/projects/*
merge_request: 41332
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for ee/spec/lib/ee/gitlab/background_migration/*
merge_request: 41357
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for spec/requests/api/*
merge_request: 41362
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix Style/RedundantSort cop
merge_request: 41108
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Flag errors from psql when restoring from backups
merge_request: 40911
author:
type: fixed

View File

@ -59,11 +59,9 @@ if gitlab.mr_labels.include?('database') || db_paths_to_review.any?
markdown(DB_MESSAGE)
markdown(DB_FILES_MESSAGE + helper.markdown_list(db_paths_to_review)) if db_paths_to_review.any?
database_labels = helper.missing_database_labels(gitlab.mr_labels)
if database_labels.any?
unless has_database_scoped_labels?(gitlab.mr_labels)
gitlab.api.update_merge_request(gitlab.mr_json['project_id'],
gitlab.mr_json['iid'],
labels: (gitlab.mr_labels + database_labels).join(','))
gitlab.mr_json['iid'],
add_labels: 'database::review pending')
end
end

View File

@ -25,9 +25,3 @@ markdown(<<~MARKDOWN)
- [Technical Writers assignments](https://about.gitlab.com/handbook/engineering/technical-writing/#designated-technical-writers) for the appropriate technical writer for this review.
- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
MARKDOWN
unless gitlab.mr_labels.include?('documentation')
gitlab.api.update_merge_request(gitlab.mr_json['project_id'],
gitlab.mr_json['iid'],
labels: (gitlab.mr_labels + ['documentation']).join(','))
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
gitlab_danger = GitlabDanger.new(helper.gitlab_helper)
return unless gitlab_danger.ci?
SPECIALIZATIONS = {
database: 'database',
backend: 'backend',
frontend: 'frontend',
docs: 'documentation',
qa: 'QA',
engineering_productivity: 'Engineering Productivity'
}.freeze
labels_to_add = helper.changes_by_category.each_with_object([]) do |(category, _changes), memo|
label = SPECIALIZATIONS.fetch(category, category.to_s)
memo << label unless gitlab.mr_labels.include?(label)
end
if labels_to_add.any?
gitlab.api.update_merge_request(gitlab.mr_json['project_id'],
gitlab.mr_json['iid'],
add_labels: labels_to_add.join(','))
end

View File

@ -123,7 +123,7 @@ Read more about [GitLab as an OAuth2 provider](oauth2.md).
### Personal/project access tokens
Access tokens can be used to authenticate with the API by passing it in either the `private_token` parameter
or the `Private-Token` header.
or the `PRIVATE-TOKEN` header.
Example of using the personal/project access token in a parameter:
@ -134,7 +134,7 @@ curl "https://gitlab.example.com/api/v4/projects?private_token=<your_access_toke
Example of using the personal/project access token in a header:
```shell
curl --header "Private-Token: <your_access_token>" "https://gitlab.example.com/api/v4/projects"
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects"
```
You can also use personal/project access tokens with OAuth-compliant headers:
@ -178,7 +178,7 @@ For more information, refer to the
[users API](users.md#create-an-impersonation-token) docs.
Impersonation tokens are used exactly like regular personal access tokens, and can be passed in either the
`private_token` parameter or the `Private-Token` header.
`private_token` parameter or the `PRIVATE-TOKEN` header.
#### Disable impersonation
@ -266,7 +266,7 @@ GET /projects?private_token=<your_access_token>&sudo=username
```
```shell
curl --header "Private-Token: <your_access_token>" --header "Sudo: username" "https://gitlab.example.com/api/v4/projects"
curl --header "PRIVATE-TOKEN: <your_access_token>" --header "Sudo: username" "https://gitlab.example.com/api/v4/projects"
```
Example of a valid API call and a request using cURL with sudo request,
@ -277,7 +277,7 @@ GET /projects?private_token=<your_access_token>&sudo=23
```
```shell
curl --header "Private-Token: <your_access_token>" --header "Sudo: 23" "https://gitlab.example.com/api/v4/projects"
curl --header "PRIVATE-TOKEN: <your_access_token>" --header "Sudo: 23" "https://gitlab.example.com/api/v4/projects"
```
## Status codes

View File

@ -31,7 +31,7 @@ GET /projects/:id/freeze_periods
Example request:
```shell
curl --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" "https://gitlab.example.com/api/v4/projects/19/freeze_periods"
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/19/freeze_periods"
```
Example response:
@ -65,7 +65,7 @@ GET /projects/:id/freeze_periods/:freeze_period_id
Example request:
```shell
curl --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" "https://gitlab.example.com/api/v4/projects/19/freeze_periods/1"
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/19/freeze_periods/1"
```
Example response:
@ -99,7 +99,7 @@ POST /projects/:id/freeze_periods
Example request:
```shell
curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" \
curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: <your_access_token>" \
--data '{ "freeze_start": "0 23 * * 5", "freeze_end": "0 7 * * 1", "cron_timezone": "UTC" }' \
--request POST https://gitlab.example.com/api/v4/projects/19/freeze_periods
```
@ -136,7 +136,7 @@ PUT /projects/:id/freeze_periods/:tag_name
Example request:
```shell
curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" \
curl --header 'Content-Type: application/json' --header "PRIVATE-TOKEN: <your_access_token>" \
--data '{ "freeze_end": "0 8 * * 1" }' \
--request PUT https://gitlab.example.com/api/v4/projects/19/freeze_periods/1
```
@ -170,6 +170,6 @@ DELETE /projects/:id/freeze_periods/:freeze_period_id
Example request:
```shell
curl --request DELETE --header "PRIVATE-TOKEN: gVWYVHDRzXiRpN1rUC8T" "https://gitlab.example.com/api/v4/projects/19/freeze_periods/1"
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/19/freeze_periods/1"
```

View File

@ -1802,6 +1802,36 @@ type ClusterAgentTokenCreatePayload {
token: ClusterAgentToken
}
"""
Autogenerated input type of ClusterAgentTokenDelete
"""
input ClusterAgentTokenDeleteInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Global ID of the cluster agent token that will be deleted
"""
id: ClustersAgentTokenID!
}
"""
Autogenerated return type of ClusterAgentTokenDelete
"""
type ClusterAgentTokenDeletePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Identifier of Clusters::Agent
"""
@ -10030,6 +10060,7 @@ type Mutation {
boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload
clusterAgentDelete(input: ClusterAgentDeleteInput!): ClusterAgentDeletePayload
clusterAgentTokenCreate(input: ClusterAgentTokenCreateInput!): ClusterAgentTokenCreatePayload
clusterAgentTokenDelete(input: ClusterAgentTokenDeleteInput!): ClusterAgentTokenDeletePayload
commitCreate(input: CommitCreateInput!): CommitCreatePayload
configureSast(input: ConfigureSastInput!): ConfigureSastPayload
createAlertIssue(input: CreateAlertIssueInput!): CreateAlertIssuePayload

View File

@ -4944,6 +4944,94 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "ClusterAgentTokenDeleteInput",
"description": "Autogenerated input type of ClusterAgentTokenDelete",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "Global ID of the cluster agent token that will be deleted",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ClustersAgentTokenID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ClusterAgentTokenDeletePayload",
"description": "Autogenerated return type of ClusterAgentTokenDelete",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "ClustersAgentID",
@ -28343,6 +28431,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "clusterAgentTokenDelete",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "ClusterAgentTokenDeleteInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ClusterAgentTokenDeletePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "commitCreate",
"description": null,

View File

@ -314,6 +314,15 @@ Autogenerated return type of ClusterAgentTokenCreate
| `secret` | String | Token secret value. Make sure you save it - you won't be able to access it again |
| `token` | ClusterAgentToken | Token created after mutation |
## ClusterAgentTokenDeletePayload
Autogenerated return type of ClusterAgentTokenDelete
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## Commit
| Name | Type | Description |

View File

@ -47,6 +47,37 @@ To protect an environment:
The protected environment will now appear in the list of protected environments.
## Environment access by group membership
A user may be granted access to protected environments as part of
[group membership](../../user/group/index.md). Users with
[Reporter permissions](../../user/permissions.md), can only be granted access to
protected environments with this method.
## Deployment branch access
Users with [Developer permissions](../../user/permissions.md) can be granted
access to a protected environment through any of these methods:
- As an individual contributor, through a role.
- Through a group membership.
If the user also has push or merge access to the branch deployed on production,
they have the following privileges:
- [Stopping an environment](index.md#stopping-an-environment).
- [Delete a stopped environment](index.md#delete-a-stopped-environment).
- [Create an environment terminal](index.md#web-terminals).
## Deployment-only access to protected environments
Users granted access to a protected environment, but not push or merge access
to the branch deployed to it, are only granted access to deploy the environment.
NOTE: **Note:**
Deployment-only access is the only possible access level for users with
[Reporter permissions](../../user/permissions.md).
## Modifying and unprotecting environments
Maintainers can:
@ -55,6 +86,10 @@ Maintainers can:
**Allowed to Deploy** dropdown menu.
- Unprotect a protected environment by clicking the **Unprotect** button for that environment.
NOTE: **Note:**
After an environment is unprotected, all access entries are deleted and must
be re-entered if the environment is re-protected.
For more information, see [Deployment safety](deployment_safety.md).
<!-- ## Troubleshooting

View File

@ -2448,7 +2448,7 @@ You can set the period with `start_in` key. The value of `start_in` key is an el
provided. `start_in` key must be less than or equal to one week. Examples of valid values include:
- `'5'`
- `10 seconds`
- `5 seconds`
- `30 minutes`
- `1 day`
- `1 week`
@ -3269,7 +3269,8 @@ and are not accessible anymore.
The value of `expire_in` is an elapsed time in seconds, unless a unit is
provided. Examples of valid values:
- `42`
- `'42'`
- `42 seconds`
- `3 mins 4 sec`
- `2 hrs 20 min`
- `2h20min`

View File

@ -305,7 +305,7 @@ Do not include the same information in multiple places.
### References across documents
- Give each folder an index.md page that introduces the topic, introduces the
- Give each folder an `index.md` page that introduces the topic, introduces the
pages within, and links to the pages within (including to the index pages of
any next-level subpaths).
- To ensure discoverability, ensure each new or renamed doc is linked from its
@ -642,8 +642,8 @@ Additional examples are available in the [Pajamas guide for punctuation](https:/
### Placeholder text
Often in examples, a writer will provide a command or configuration that is
complete apart from a value specific to the reader.
Often in examples, a writer will provide a command or configuration that
uses values specific to the reader.
In these cases, use [`<` and `>`](https://en.wikipedia.org/wiki/Usage_message#Pattern)
to call out where a reader must replace text with their own value.
@ -1943,7 +1943,7 @@ METHOD /endpoint
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/endpoint?parameters'
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/endpoint?parameters"
```
Example response:
@ -2038,7 +2038,7 @@ you can use in the API documentation.
Get the details of a group:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/gitlab-org
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/gitlab-org"
```
#### cURL example with parameters passed in the URL
@ -2066,7 +2066,7 @@ In this example we create a new group. Watch carefully the single and double
quotes.
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v4/groups
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' "https://gitlab.example.com/api/v4/groups"
```
#### Post data using form-data
@ -2075,7 +2075,7 @@ Instead of using JSON or urlencode you can use multipart/form-data which
properly handles data encoding:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v4/users/25/keys
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." "https://gitlab.example.com/api/v4/users/25/keys"
```
The above example is run by and administrator and will add an SSH public key
@ -2101,7 +2101,7 @@ exclude specific users when requesting a list of users for a project, you would
do something like this:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "skip_users[]=<user_id>" --data "skip_users[]=<user_id>" https://gitlab.example.com/api/v4/projects/<project_id>/users
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "skip_users[]=<user_id>" --data "skip_users[]=<user_id>" "https://gitlab.example.com/api/v4/projects/<project_id>/users"
```
## GraphQL API

View File

@ -524,6 +524,48 @@ for modifications. If you have no other choice, an `around` block similar to the
example for global variables, above, can be used, but this should be avoided if
at all possible.
#### Test Snowplow events
CAUTION: **Warning:**
Snowplow performs **runtime type checks** by using the [contracts gem](https://rubygems.org/gems/contracts).
Since Snowplow is **by default disabled in tests and development**, it can be hard to
**catch exceptions** when mocking `Gitlab::Tracking`.
To catch runtime errors due to type checks, you can enable Snowplow in tests by marking the spec with
`:snowplow` and use the `expect_snowplow_event` helper which will check for
calls to `Gitlab::Tracking#event`.
```ruby
describe '#show', :snowplow do
it 'tracks snowplow events' do
get :show
expect_snowplow_event(
category: 'Experiment',
action: 'start',
)
expect_snowplow_event(
category: 'Experiment',
action: 'sent',
property: 'property',
label: 'label'
)
end
end
```
When you want to ensure that no event got called, you can use `expect_no_snowplow_event`.
```ruby
describe '#show', :snowplow do
it 'does not track any snowplow events' do
get :show
expect_no_snowplow_event
end
end
```
### Table-based / Parameterized tests
This style of testing is used to exercise one piece of code with a comprehensive

View File

@ -11,6 +11,12 @@ Navigate to the Alert details view by visiting the
list. You need least Developer [permissions](../../user/permissions.md) to access
alerts.
TIP: **Tip:**
To review live examples of GitLab alerts, visit the
[alert list](https://gitlab.com/gitlab-examples/ops/incident-setup/everyone/tanuki-inc/-/alert_management)
for this demo project. Click any alert in the list to examine its alert details
page.
Alerts provide **Overview** and **Alert details** tabs to give you the right
amount of information you need.

View File

@ -31,6 +31,10 @@ The alert list displays the following information:
- **Triggered**: No one has begun investigation.
- **Acknowledged**: Someone is actively investigating the problem.
- **Resolved**: No further work is required.
TIP: **Tip:**
Check out a [live example](https://gitlab.com/gitlab-examples/ops/incident-setup/everyone/tanuki-inc/-/alert_management)
in GitLab to examine alerts in action.
## Enable Alerts

View File

@ -33,6 +33,10 @@ The Incident list displays incidents sorted by incident created date.
To see if a column is sortable, point your mouse at the header. Sortable columns
display an arrow next to the column name.
TIP: **Tip:**
For a live example of the incident list in action, visit this
[demo project](https://gitlab.com/gitlab-examples/ops/incident-setup/everyone/tanuki-inc/-/incidents).
NOTE: **Note:**
Incidents share the [Issues API](../../user/project/issues/index.md).

View File

@ -295,6 +295,24 @@ For installations from source:
sudo -u git -H bundle exec rake gitlab:backup:create SKIP=tar RAILS_ENV=production
```
#### Disabling prompts during restore
During a restore from backup, the restore script may ask for confirmation before
proceeding. If you wish to disable these prompts, you can set the `GITLAB_ASSUME_YES`
environment variable to `1`.
For Omnibus GitLab packages:
```shell
sudo GITLAB_ASSUME_YES=1 gitlab-backup restore
```
For installations from source:
```shell
sudo -u git -H GITLAB_ASSUME_YES=1 bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
#### Back up Git repositories concurrently
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37158) in GitLab 13.3.

View File

@ -68,7 +68,7 @@ Below are the settings for [GitLab Pages](https://about.gitlab.com/stages-devops
| IP address | `35.185.44.232` | - |
| Custom domains support | yes | no |
| TLS certificates support | yes | no |
| Maximum size (uncompressed) | 1G | 100M |
| Maximum size (compressed) | 1G | 100M |
NOTE: **Note:**
The maximum size of your Pages site is regulated by the artifacts maximum size
@ -80,7 +80,7 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
| Setting | GitLab.com | Default |
| ----------- | ----------------- | ------------- |
| Artifacts maximum size (uncompressed) | 1G | 100M |
| Artifacts maximum size (compressed) | 1G | 100M |
| Artifacts [expiry time](../../ci/yaml/README.md#artifactsexpire_in) | From June 22, 2020, deleted after 30 days unless otherwise specified (artifacts created before that date have no expiry). | deleted after 30 days unless otherwise specified |
| Scheduled Pipeline Cron | `*/5 * * * *` | `19 * * * *` |
| [Max jobs in active pipelines](../../administration/instance_limits.md#number-of-jobs-in-active-pipelines) | `500` for Free tier, unlimited otherwise | Unlimited

View File

@ -6,16 +6,6 @@ module API
extend Grape::API::Helpers
include ::API::Helpers::PackagesHelpers
params :workhorse_upload_params do
optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)'
optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
end
def find_job_from_http_basic_auth
return unless request.headers
@ -36,12 +26,6 @@ module API
DeployToken.active.find_by_token(token)
end
def uploaded_package_file(param_name = :file)
uploaded_file = UploadedFile.from_params(params, param_name, ::Packages::PackageFileUploader.workhorse_local_upload_path)
bad_request!('Missing package file!') unless uploaded_file
uploaded_file
end
private
def decode_token

View File

@ -8,10 +8,10 @@ module Backup
attr_reader :progress
attr_reader :config, :db_file_name
def initialize(progress)
def initialize(progress, filename: nil)
@progress = progress
@config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
@db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
@db_file_name = filename || File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
end
def dump
@ -57,26 +57,63 @@ module Backup
decompress_pid = spawn(*%w(gzip -cd), out: decompress_wr, in: db_file_name)
decompress_wr.close
restore_pid =
status, errors =
case config["adapter"]
when "postgresql" then
progress.print "Restoring PostgreSQL database #{config['database']} ... "
pg_env
spawn('psql', config['database'], in: decompress_rd)
execute_and_track_errors(pg_restore_cmd, decompress_rd)
end
decompress_rd.close
success = [decompress_pid, restore_pid].all? do |pid|
Process.waitpid(pid)
$?.success?
Process.waitpid(decompress_pid)
success = $?.success? && status.success?
if errors.present?
progress.print "------ BEGIN ERRORS -----".color(:yellow)
progress.print errors.join.color(:yellow)
progress.print "------ END ERRORS -------".color(:yellow)
end
report_success(success)
abort 'Restore failed' unless success
raise Backup::Error, 'Restore failed' unless success
errors
end
protected
def execute_and_track_errors(cmd, decompress_rd)
errors = []
Open3.popen3(ENV, *cmd) do |stdin, stdout, stderr, thread|
stdin.binmode
Thread.new do
data = stdout.read
$stdout.write(data)
end
Thread.new do
until (raw_line = stderr.gets).nil?
warn(raw_line)
# Recent database dumps will use --if-exists with pg_dump
errors << raw_line unless raw_line =~ /does not exist$/
end
end
begin
IO.copy_stream(decompress_rd, stdin)
rescue Errno::EPIPE
end
stdin.close
thread.join
[thread.value, errors]
end
end
def pg_env
args = {
'username' => 'PGUSER',
@ -101,5 +138,11 @@ module Backup
progress.puts '[FAILED]'.color(:red)
end
end
private
def pg_restore_cmd
['psql', config['database']]
end
end
end

View File

@ -206,16 +206,6 @@ module Gitlab
usernames.map { |u| Gitlab::Danger::Teammate.new('username' => u) }
end
def missing_database_labels(current_mr_labels)
labels = if has_database_scoped_labels?(current_mr_labels)
['database']
else
['database', 'database::review pending']
end
labels - current_mr_labels
end
def sanitize_mr_title(title)
title.gsub(DRAFT_REGEX, '').gsub(/`/, '\\\`')
end
@ -259,8 +249,6 @@ module Gitlab
all_changed_files.grep(regex)
end
private
def has_database_scoped_labels?(current_mr_labels)
current_mr_labels.any? { |label| label.start_with?('database::') }
end

View File

@ -24,6 +24,8 @@ module Gitlab
# Returns "yes" the user chose to continue
# Raises Gitlab::TaskAbortedByUserError if the user chose *not* to continue
def ask_to_continue
return if Gitlab::Utils.to_boolean(ENV['GITLAB_ASSUME_YES'])
answer = prompt("Do you want to continue (yes/no)? ".color(:blue), %w{yes no})
raise Gitlab::TaskAbortedByUserError unless answer == "yes"
end

View File

@ -56,12 +56,19 @@ module Gitlab
def snowplow
@snowplow ||= SnowplowTracker::Tracker.new(
SnowplowTracker::AsyncEmitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname, protocol: 'https'),
emitter,
SnowplowTracker::Subject.new,
SNOWPLOW_NAMESPACE,
Gitlab::CurrentSettings.snowplow_app_id
)
end
def emitter
SnowplowTracker::AsyncEmitter.new(
Gitlab::CurrentSettings.snowplow_collector_hostname,
protocol: 'https'
)
end
end
end
end

View File

@ -22,6 +22,7 @@ class GitlabDanger
roulette
ce_ee_vue_templates
sidekiq_queues
specialization_labels
].freeze
MESSAGE_PREFIX = '==>'.freeze

View File

@ -136,7 +136,21 @@ namespace :gitlab do
task restore: :gitlab_environment do
puts_time "Restoring database ... ".color(:blue)
Backup::Database.new(progress).restore
errors = Backup::Database.new(progress).restore
if errors.present?
warning = <<~MSG
There were errors in restoring the schema. This may cause
issues if this results in missing indexes, constraints, or
columns. Please record the errors above and contact GitLab
Support if you have questions:
https://about.gitlab.com/support/
MSG
warn warning.color(:red)
ask_to_continue
end
puts_time "done".color(:green)
end
end

View File

@ -7734,12 +7734,21 @@ msgstr ""
msgid "DastProfiles|Could not create the site profile. Please try again."
msgstr ""
msgid "DastProfiles|Could not delete scanner profile. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not delete scanner profiles:"
msgstr ""
msgid "DastProfiles|Could not delete site profile. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not delete site profiles:"
msgstr ""
msgid "DastProfiles|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later."
msgstr ""
@ -7752,6 +7761,9 @@ msgstr ""
msgid "DastProfiles|Do you want to discard your changes?"
msgstr ""
msgid "DastProfiles|Edit feature will come soon. Please create a new profile if changes needed"
msgstr ""
msgid "DastProfiles|Edit site profile"
msgstr ""
@ -7794,6 +7806,9 @@ msgstr ""
msgid "DastProfiles|Scanner Profile"
msgstr ""
msgid "DastProfiles|Scanner Profiles"
msgstr ""
msgid "DastProfiles|Site Profile"
msgstr ""

View File

@ -40,7 +40,7 @@ FactoryBot.define do
forward_deployment_enabled { nil }
end
after(:create) do |project, evaluator|
before(:create) do |project, evaluator|
# Builds and MRs can't have higher visibility level than repository access level.
builds_access_level = [evaluator.builds_access_level, evaluator.repository_access_level].min
merge_requests_access_level = [evaluator.merge_requests_access_level, evaluator.repository_access_level].min
@ -56,8 +56,10 @@ FactoryBot.define do
pages_access_level: evaluator.pages_access_level
}
project.project_feature.update!(hash)
project.build_project_feature(hash)
end
after(:create) do |project, evaluator|
# Normally the class Projects::CreateService is used for creating
# projects, and this class takes care of making sure the owner and current
# user have access to the project. Our specs don't use said service class,

View File

@ -19,8 +19,9 @@ describe('Ajax Loading Spinner', () => {
req.beforeSend(xhr, { dataType: 'text/html' });
expect(icon).not.toHaveClass('fa-trash-o');
expect(icon).toHaveClass('fa-spinner');
expect(icon).toHaveClass('fa-spin');
expect(icon).toHaveClass('gl-spinner');
expect(icon).toHaveClass('gl-spinner-orange');
expect(icon).toHaveClass('gl-spinner-sm');
expect(icon.dataset.icon).toEqual('fa-trash-o');
expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual('');
@ -44,8 +45,9 @@ describe('Ajax Loading Spinner', () => {
const icon = ajaxLoadingSpinner.querySelector('i');
expect(icon).toHaveClass('fa-trash-o');
expect(icon).not.toHaveClass('fa-spinner');
expect(icon).not.toHaveClass('fa-spin');
expect(icon).not.toHaveClass('gl-spinner');
expect(icon).not.toHaveClass('gl-spinner-orange');
expect(icon).not.toHaveClass('gl-spinner-sm');
expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null);
done();

View File

@ -11,6 +11,7 @@ describe('board_form.vue', () => {
const propsData = {
canAdminBoard: false,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/-/labels`,
};
const findModal = () => wrapper.find(DeprecatedModal);

View File

@ -86,6 +86,7 @@ describe('BoardsSelector', () => {
canAdminBoard: true,
multipleIssueBoardsAvailable: true,
labelsPath: `${TEST_HOST}/labels/path`,
labelsWebUrl: `${TEST_HOST}/labels`,
projectId: 42,
groupId: 19,
scopedIssueBoardFeatureEnabled: true,

View File

@ -67,7 +67,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
<p>
<code
class="ws-pre-wrap"
class="gl-white-space-pre-wrap"
>
foo
</code>

View File

@ -56,7 +56,7 @@ exports[`Project remove modal intialized matches the snapshot 1`] = `
<p>
<code
class="ws-pre-wrap"
class="gl-white-space-pre-wrap"
>
foo
</code>

View File

@ -20,6 +20,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</label>
<gl-form-group-stub
class="gl-mb-0"
id="visibility-level-setting"
>
<gl-form-radio-group-stub
@ -90,5 +91,12 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
</gl-form-radio-stub>
</gl-form-radio-group-stub>
</gl-form-group-stub>
<div
class="text-muted"
data-testid="restricted-levels-info"
>
<!---->
</div>
</div>
`;

View File

@ -102,6 +102,13 @@ describe('Snippet Edit app', () => {
markdownDocsPath: 'http://docs.foo.bar',
...props,
},
data() {
return {
snippet: {
visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
},
};
},
});
}

View File

@ -1,31 +1,55 @@
import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import { defaultSnippetVisibilityLevels } from '~/snippets/utils/blob';
import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
SNIPPET_LEVELS_RESTRICTED,
SNIPPET_LEVELS_DISABLED,
} from '~/snippets/constants';
describe('Snippet Visibility Edit component', () => {
let wrapper;
const defaultHelpLink = '/foo/bar';
const defaultVisibilityLevel = 'private';
const defaultVisibility = defaultSnippetVisibilityLevels([0, 10, 20]);
function createComponent(propsData = {}, deep = false) {
function createComponent({
propsData = {},
visibilityLevels = defaultVisibility,
multipleLevelsRestricted = false,
deep = false,
} = {}) {
const method = deep ? mount : shallowMount;
const $apollo = {
queries: {
defaultVisibility: {
loading: false,
},
},
};
wrapper = method.call(this, SnippetVisibilityEdit, {
mock: { $apollo },
propsData: {
helpLink: defaultHelpLink,
isProjectSnippet: false,
value: defaultVisibilityLevel,
...propsData,
},
data() {
return {
visibilityLevels,
multipleLevelsRestricted,
};
},
});
}
const findLabel = () => wrapper.find('label');
const findLink = () => wrapper.find('label').find(GlLink);
const findRadios = () => wrapper.find(GlFormRadioGroup).findAll(GlFormRadio);
const findRadiosData = () =>
findRadios().wrappers.map(x => {
@ -47,60 +71,84 @@ describe('Snippet Visibility Edit component', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders visibility options', () => {
createComponent({}, true);
it('renders label help link', () => {
createComponent();
expect(findRadiosData()).toEqual([
{
expect(findLink().attributes('href')).toBe(defaultHelpLink);
});
it('when helpLink is not defined, does not render label help link', () => {
createComponent({ propsData: { helpLink: null } });
expect(findLink().exists()).toBe(false);
});
describe('Visibility options', () => {
const findRestrictedInfo = () => wrapper.find('[data-testid="restricted-levels-info"]');
const RESULTING_OPTIONS = {
0: {
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description,
},
{
10: {
value: SNIPPET_VISIBILITY_INTERNAL,
icon: SNIPPET_VISIBILITY.internal.icon,
text: SNIPPET_VISIBILITY.internal.label,
description: SNIPPET_VISIBILITY.internal.description,
},
{
20: {
value: SNIPPET_VISIBILITY_PUBLIC,
icon: SNIPPET_VISIBILITY.public.icon,
text: SNIPPET_VISIBILITY.public.label,
description: SNIPPET_VISIBILITY.public.description,
},
]);
});
};
it('when project snippet, renders special private description', () => {
createComponent({ isProjectSnippet: true }, true);
expect(findRadiosData()[0]).toEqual({
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description_project,
it.each`
levels | resultOptions
${undefined} | ${[]}
${''} | ${[]}
${[]} | ${[]}
${[0]} | ${[RESULTING_OPTIONS[0]]}
${[0, 10]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10]]}
${[0, 10, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]}
${[0, 20]} | ${[RESULTING_OPTIONS[0], RESULTING_OPTIONS[20]]}
${[10, 20]} | ${[RESULTING_OPTIONS[10], RESULTING_OPTIONS[20]]}
`('renders correct visibility options for $levels', ({ levels, resultOptions }) => {
createComponent({ visibilityLevels: defaultSnippetVisibilityLevels(levels), deep: true });
expect(findRadiosData()).toEqual(resultOptions);
});
});
it('renders label help link', () => {
createComponent();
it.each`
levels | levelsRestricted | resultText
${[]} | ${false} | ${SNIPPET_LEVELS_DISABLED}
${[]} | ${true} | ${SNIPPET_LEVELS_DISABLED}
${[0]} | ${true} | ${SNIPPET_LEVELS_RESTRICTED}
${[0]} | ${false} | ${''}
${[0, 10, 20]} | ${false} | ${''}
`(
'renders correct information about restricted visibility levels for $levels',
({ levels, levelsRestricted, resultText }) => {
createComponent({
visibilityLevels: defaultSnippetVisibilityLevels(levels),
multipleLevelsRestricted: levelsRestricted,
});
expect(findRestrictedInfo().text()).toBe(resultText);
},
);
expect(
findLabel()
.find(GlLink)
.attributes('href'),
).toBe(defaultHelpLink);
});
it('when project snippet, renders special private description', () => {
createComponent({ propsData: { isProjectSnippet: true }, deep: true });
it('when helpLink is not defined, does not render label help link', () => {
createComponent({ helpLink: null });
expect(
findLabel()
.find(GlLink)
.exists(),
).toBe(false);
expect(findRadiosData()[0]).toEqual({
value: SNIPPET_VISIBILITY_PRIVATE,
icon: SNIPPET_VISIBILITY.private.icon,
text: SNIPPET_VISIBILITY.private.label,
description: SNIPPET_VISIBILITY.private.description_project,
});
});
});
});
@ -108,7 +156,7 @@ describe('Snippet Visibility Edit component', () => {
it('pre-selects correct option in the list', () => {
const value = SNIPPET_VISIBILITY_INTERNAL;
createComponent({ value });
createComponent({ propsData: { value } });
expect(wrapper.find(GlFormRadioGroup).attributes('checked')).toBe(value);
});

View File

@ -14,6 +14,7 @@ const createComponent = headerTitle => {
};
describe('DropdownCreateLabelComponent', () => {
const colorsCount = Object.keys(mockSuggestedColors).length;
let vm;
beforeEach(() => {
@ -27,7 +28,7 @@ describe('DropdownCreateLabelComponent', () => {
describe('created', () => {
it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length);
expect(vm.suggestedColors.length).toBe(colorsCount);
});
});
@ -78,11 +79,11 @@ describe('DropdownCreateLabelComponent', () => {
const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
expect(colorsListContainerEl).not.toBe(null);
expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length);
expect(colorsListContainerEl.querySelectorAll('a').length).toBe(colorsCount);
const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]);
expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0].colorCode);
expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);');
});

View File

@ -15,29 +15,29 @@ export const mockLabels = [
},
];
export const mockSuggestedColors = [
'#0033CC',
'#428BCA',
'#44AD8E',
'#A8D695',
'#5CB85C',
'#69D100',
'#004E00',
'#34495E',
'#7F8C8D',
'#A295D6',
'#5843AD',
'#8E44AD',
'#FFECDB',
'#AD4363',
'#D10069',
'#CC0033',
'#FF0000',
'#D9534F',
'#D1D100',
'#F0AD4E',
'#AD8D43',
];
export const mockSuggestedColors = {
'#0033CC': 'UA blue',
'#428BCA': 'Moderate blue',
'#44AD8E': 'Lime green',
'#A8D695': 'Feijoa',
'#5CB85C': 'Slightly desaturated green',
'#69D100': 'Bright green',
'#004E00': 'Very dark lime green',
'#34495E': 'Very dark desaturated blue',
'#7F8C8D': 'Dark grayish cyan',
'#A295D6': 'Slightly desaturated blue',
'#5843AD': 'Dark moderate blue',
'#8E44AD': 'Dark moderate violet',
'#FFECDB': 'Very pale orange',
'#AD4363': 'Dark moderate pink',
'#D10069': 'Strong pink',
'#CC0033': 'Strong red',
'#FF0000': 'Pure red',
'#D9534F': 'Soft red',
'#D1D100': 'Strong yellow',
'#F0AD4E': 'Soft orange',
'#AD8D43': 'Dark moderate orange',
};
export const mockConfig = {
showCreate: true,

View File

@ -86,36 +86,4 @@ RSpec.describe API::Helpers::PackagesManagerClientsHelpers do
it_behaves_like 'invalid auth header'
end
describe '#uploaded_package_file' do
let_it_be(:params) { {} }
subject { helper.uploaded_package_file }
before do
allow(helper).to receive(:params).and_return(params)
end
context 'with valid uploaded package file' do
let_it_be(:uploaded_file) { Object.new }
before do
allow(UploadedFile).to receive(:from_params).and_return(uploaded_file)
end
it { is_expected.to be uploaded_file }
end
context 'with invalid uploaded package file' do
before do
allow(UploadedFile).to receive(:from_params).and_return(nil)
end
it 'fails with bad_request!' do
expect(helper).to receive(:bad_request!)
expect(subject).to be nil
end
end
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Backup::Database do
let(:progress) { StringIO.new }
let(:output) { progress.string }
describe '#restore' do
let(:cmd) { %W[#{Gem.ruby} -e $stdout.puts(1)] }
let(:data) { Rails.root.join("spec/fixtures/pages_empty.tar.gz").to_s }
subject { described_class.new(progress, filename: data) }
before do
allow(subject).to receive(:pg_restore_cmd).and_return(cmd)
end
context 'with an empty .gz file' do
let(:data) { Rails.root.join("spec/fixtures/pages_empty.tar.gz").to_s }
it 'returns successfully' do
expect(subject.restore).to eq([])
expect(output).to include("Restoring PostgreSQL database")
expect(output).to include("[DONE]")
expect(output).not_to include("ERRORS")
end
end
context 'with a corrupted .gz file' do
let(:data) { Rails.root.join("spec/fixtures/big-image.png").to_s }
it 'raises a backup error' do
expect { subject.restore }.to raise_error(Backup::Error)
end
end
context 'when the restore command prints errors' do
let(:visible_error) { "This is a test error\n" }
let(:noise) { "Table projects does not exist\n" }
let(:cmd) { %W[#{Gem.ruby} -e $stderr.write("#{noise}#{visible_error}")] }
it 'filters out noise from errors' do
expect(subject.restore).to eq([visible_error])
expect(output).to include("ERRORS")
expect(output).not_to include(noise)
expect(output).to include(visible_error)
end
end
end
end

View File

@ -371,22 +371,6 @@ RSpec.describe Gitlab::Danger::Helper do
end
end
describe '#missing_database_labels' do
subject { helper.missing_database_labels(current_mr_labels) }
context 'when current merge request has ~database::review pending' do
let(:current_mr_labels) { ['database::review pending', 'feature'] }
it { is_expected.to match_array(['database']) }
end
context 'when current merge request does not have ~database::review pending' do
let(:current_mr_labels) { ['feature'] }
it { is_expected.to match_array(['database', 'database::review pending']) }
end
end
describe '#sanitize_mr_title' do
where(:mr_title, :expected_mr_title) do
'My MR title' | 'My MR title'

View File

@ -2945,6 +2945,22 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
describe '#has_coverage_reports?' do
subject { pipeline.has_coverage_reports? }
context 'when pipeline has a code coverage artifact' do
let(:pipeline) { create(:ci_pipeline, :with_coverage_report_artifact, :running, project: project) }
it { expect(subject).to be_truthy }
end
context 'when pipeline does not have a code coverage artifact' do
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { expect(subject).to be_falsey }
end
end
describe '#can_generate_coverage_reports?' do
subject { pipeline.can_generate_coverage_reports? }
context 'when pipeline has builds with coverage reports' do
before do
create(:ci_build, :coverage_reports, pipeline: pipeline, project: project)

View File

@ -15,17 +15,17 @@ RSpec.describe 'CycleAnalytics#issue' do
generate_cycle_analytics_spec(
phase: :issue,
data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]],
start_time_conditions: [["issue created", -> (context, data) { data[:issue].save! }]],
end_time_conditions: [["issue associated with a milestone",
-> (context, data) do
if data[:issue].persisted?
data[:issue].update(milestone: context.create(:milestone, project: context.project))
data[:issue].update!(milestone: context.create(:milestone, project: context.project))
end
end],
["list label added to issue",
-> (context, data) do
if data[:issue].persisted?
data[:issue].update(label_ids: [context.create(:list).label_id])
data[:issue].update!(label_ids: [context.create(:list).label_id])
end
end]],
post_fn: -> (context, data) do
@ -35,7 +35,7 @@ RSpec.describe 'CycleAnalytics#issue' do
it "returns nil" do
regular_label = create(:label)
issue = create(:issue, project: project)
issue.update(label_ids: [regular_label.id])
issue.update!(label_ids: [regular_label.id])
create_merge_request_closing_issue(user, project, issue)
merge_merge_requests_closing_issue(user, project, issue)

View File

@ -22,11 +22,11 @@ RSpec.describe 'CycleAnalytics#plan' do
end,
start_time_conditions: [["issue associated with a milestone",
-> (context, data) do
data[:issue].update(milestone: context.create(:milestone, project: context.project))
data[:issue].update!(milestone: context.create(:milestone, project: context.project))
end],
["list label added to issue",
-> (context, data) do
data[:issue].update(label_ids: [context.create(:list).label_id])
data[:issue].update!(label_ids: [context.create(:list).label_id])
end]],
end_time_conditions: [["issue mentioned in a commit",
-> (context, data) do
@ -40,7 +40,7 @@ RSpec.describe 'CycleAnalytics#plan' do
branch_name = generate(:branch)
label = create(:label)
issue = create(:issue, project: project)
issue.update(label_ids: [label.id])
issue.update!(label_ids: [label.id])
create_commit_referencing_issue(issue, branch_name: branch_name)
create_merge_request_closing_issue(user, project, issue, source_branch: branch_name)

Some files were not shown because too many files have changed in this diff Show More