Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
03a5217322
commit
4b9ace6c1f
|
@ -1 +1 @@
|
||||||
15c2f3921c4729e9c4d7ce8592300decfcfdb2e6
|
12dcff902c9a2178fa6f4992d9d562ad9b422dd2
|
||||||
|
|
2
Gemfile
2
Gemfile
|
@ -93,7 +93,7 @@ gem 'graphql', '~> 1.10.5'
|
||||||
# TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 is released:
|
# TODO: remove app/views/graphiql/rails/editors/show.html.erb when https://github.com/rmosolgo/graphiql-rails/pull/71 is released:
|
||||||
# https://gitlab.com/gitlab-org/gitlab/issues/31747
|
# https://gitlab.com/gitlab-org/gitlab/issues/31747
|
||||||
gem 'graphiql-rails', '~> 1.4.10'
|
gem 'graphiql-rails', '~> 1.4.10'
|
||||||
gem 'apollo_upload_server', '~> 2.0.0.beta3'
|
gem 'apollo_upload_server', '~> 2.0.2'
|
||||||
gem 'graphql-docs', '~> 1.6.0', group: [:development, :test]
|
gem 'graphql-docs', '~> 1.6.0', group: [:development, :test]
|
||||||
|
|
||||||
# Disable strong_params so that Mash does not respond to :permitted?
|
# Disable strong_params so that Mash does not respond to :permitted?
|
||||||
|
|
|
@ -73,7 +73,7 @@ GEM
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
aes_key_wrap (1.0.1)
|
aes_key_wrap (1.0.1)
|
||||||
akismet (3.0.0)
|
akismet (3.0.0)
|
||||||
apollo_upload_server (2.0.0.beta.3)
|
apollo_upload_server (2.0.2)
|
||||||
graphql (>= 1.8)
|
graphql (>= 1.8)
|
||||||
rails (>= 4.2)
|
rails (>= 4.2)
|
||||||
asana (0.10.0)
|
asana (0.10.0)
|
||||||
|
@ -1220,7 +1220,7 @@ DEPENDENCIES
|
||||||
acts-as-taggable-on (~> 6.0)
|
acts-as-taggable-on (~> 6.0)
|
||||||
addressable (~> 2.7)
|
addressable (~> 2.7)
|
||||||
akismet (~> 3.0)
|
akismet (~> 3.0)
|
||||||
apollo_upload_server (~> 2.0.0.beta3)
|
apollo_upload_server (~> 2.0.2)
|
||||||
asana (= 0.10.0)
|
asana (= 0.10.0)
|
||||||
asciidoctor (~> 2.0.10)
|
asciidoctor (~> 2.0.10)
|
||||||
asciidoctor-include-ext (~> 0.3.1)
|
asciidoctor-include-ext (~> 0.3.1)
|
||||||
|
|
|
@ -1,16 +1,24 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||||
import { GlDeprecatedButton, GlIcon } from '@gitlab/ui';
|
import { GlButtonGroup, GlButton, GlDropdown } from '@gitlab/ui';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
GlDeprecatedButton,
|
GlButtonGroup,
|
||||||
GlIcon,
|
GlButton,
|
||||||
|
GlDropdown,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
|
...mapGetters('diffs', ['isInlineView', 'isParallelView']),
|
||||||
...mapState('diffs', ['renderTreeList', 'showWhitespace']),
|
...mapState('diffs', ['renderTreeList', 'showWhitespace']),
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.patchAriaLabel();
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
this.patchAriaLabel();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('diffs', [
|
...mapActions('diffs', [
|
||||||
'setInlineDiffViewType',
|
'setInlineDiffViewType',
|
||||||
|
@ -18,65 +26,61 @@ export default {
|
||||||
'setRenderTreeList',
|
'setRenderTreeList',
|
||||||
'setShowWhitespace',
|
'setShowWhitespace',
|
||||||
]),
|
]),
|
||||||
|
patchAriaLabel() {
|
||||||
|
this.$el
|
||||||
|
.querySelector('.js-show-diff-settings')
|
||||||
|
.setAttribute('aria-label', __('Diff view settings'));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="dropdown">
|
<gl-dropdown icon="settings" toggle-class="js-show-diff-settings" right>
|
||||||
<button
|
<div class="gl-px-3">
|
||||||
type="button"
|
<span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('File browser') }}</span>
|
||||||
class="btn btn-default js-show-diff-settings"
|
<gl-button-group class="gl-display-flex">
|
||||||
data-toggle="dropdown"
|
<gl-button
|
||||||
data-display="static"
|
:class="{ selected: !renderTreeList }"
|
||||||
>
|
class="gl-w-half js-list-view"
|
||||||
<gl-icon name="settings" /> <gl-icon name="chevron-down" />
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3">
|
|
||||||
<div>
|
|
||||||
<span class="bold d-block mb-1">{{ __('File browser') }}</span>
|
|
||||||
<div class="btn-group d-flex">
|
|
||||||
<gl-deprecated-button
|
|
||||||
:class="{ active: !renderTreeList }"
|
|
||||||
class="w-100 js-list-view"
|
|
||||||
@click="setRenderTreeList(false)"
|
@click="setRenderTreeList(false)"
|
||||||
>
|
>
|
||||||
{{ __('List view') }}
|
{{ __('List view') }}
|
||||||
</gl-deprecated-button>
|
</gl-button>
|
||||||
<gl-deprecated-button
|
<gl-button
|
||||||
:class="{ active: renderTreeList }"
|
:class="{ selected: renderTreeList }"
|
||||||
class="w-100 js-tree-view"
|
class="gl-w-half js-tree-view"
|
||||||
@click="setRenderTreeList(true)"
|
@click="setRenderTreeList(true)"
|
||||||
>
|
>
|
||||||
{{ __('Tree view') }}
|
{{ __('Tree view') }}
|
||||||
</gl-deprecated-button>
|
</gl-button>
|
||||||
|
</gl-button-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="gl-mt-3 gl-px-3">
|
||||||
<div class="mt-2">
|
<span class="gl-font-weight-bold gl-display-block gl-mb-2">{{ __('Compare changes') }}</span>
|
||||||
<span class="bold d-block mb-1">{{ __('Compare changes') }}</span>
|
<gl-button-group class="gl-display-flex js-diff-view-buttons">
|
||||||
<div class="btn-group d-flex js-diff-view-buttons">
|
<gl-button
|
||||||
<gl-deprecated-button
|
|
||||||
id="inline-diff-btn"
|
id="inline-diff-btn"
|
||||||
:class="{ active: isInlineView }"
|
:class="{ selected: isInlineView }"
|
||||||
class="w-100 js-inline-diff-button"
|
class="gl-w-half js-inline-diff-button"
|
||||||
data-view-type="inline"
|
data-view-type="inline"
|
||||||
@click="setInlineDiffViewType"
|
@click="setInlineDiffViewType"
|
||||||
>
|
>
|
||||||
{{ __('Inline') }}
|
{{ __('Inline') }}
|
||||||
</gl-deprecated-button>
|
</gl-button>
|
||||||
<gl-deprecated-button
|
<gl-button
|
||||||
id="parallel-diff-btn"
|
id="parallel-diff-btn"
|
||||||
:class="{ active: isParallelView }"
|
:class="{ selected: isParallelView }"
|
||||||
class="w-100 js-parallel-diff-button"
|
class="gl-w-half js-parallel-diff-button"
|
||||||
data-view-type="parallel"
|
data-view-type="parallel"
|
||||||
@click="setParallelDiffViewType"
|
@click="setParallelDiffViewType"
|
||||||
>
|
>
|
||||||
{{ __('Side-by-side') }}
|
{{ __('Side-by-side') }}
|
||||||
</gl-deprecated-button>
|
</gl-button>
|
||||||
|
</gl-button-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="gl-mt-3 gl-px-3">
|
||||||
<div class="mt-2">
|
<label class="gl-mb-0">
|
||||||
<label class="mb-0">
|
|
||||||
<input
|
<input
|
||||||
id="show-whitespace"
|
id="show-whitespace"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -86,6 +90,5 @@ export default {
|
||||||
{{ __('Show whitespace changes') }}
|
{{ __('Show whitespace changes') }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</gl-dropdown>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -20,7 +20,6 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
GlIcon,
|
GlIcon,
|
||||||
GlIntersectionObserver,
|
GlIntersectionObserver,
|
||||||
descriptionComponent,
|
|
||||||
titleComponent,
|
titleComponent,
|
||||||
editedComponent,
|
editedComponent,
|
||||||
formComponent,
|
formComponent,
|
||||||
|
@ -152,6 +151,18 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
descriptionComponent: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: () => {
|
||||||
|
return descriptionComponent;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
showTitleBorder: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const store = new Store({
|
const store = new Store({
|
||||||
|
@ -209,6 +220,11 @@ export default {
|
||||||
isOpenStatus() {
|
isOpenStatus() {
|
||||||
return this.issuableStatus === IssuableStatus.Open;
|
return this.issuableStatus === IssuableStatus.Open;
|
||||||
},
|
},
|
||||||
|
pinnedLinkClasses() {
|
||||||
|
return this.showTitleBorder
|
||||||
|
? 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6'
|
||||||
|
: '';
|
||||||
|
},
|
||||||
statusIcon() {
|
statusIcon() {
|
||||||
return this.isOpenStatus ? 'issue-open-m' : 'mobile-issue-close';
|
return this.isOpenStatus ? 'issue-open-m' : 'mobile-issue-close';
|
||||||
},
|
},
|
||||||
|
@ -447,9 +463,11 @@ export default {
|
||||||
<pinned-links
|
<pinned-links
|
||||||
:zoom-meeting-url="zoomMeetingUrl"
|
:zoom-meeting-url="zoomMeetingUrl"
|
||||||
:published-incident-url="publishedIncidentUrl"
|
:published-incident-url="publishedIncidentUrl"
|
||||||
|
:class="pinnedLinkClasses"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<description-component
|
<component
|
||||||
|
:is="descriptionComponent"
|
||||||
v-if="state.descriptionHtml"
|
v-if="state.descriptionHtml"
|
||||||
:can-update="canUpdate"
|
:can-update="canUpdate"
|
||||||
:description-html="state.descriptionHtml"
|
:description-html="state.descriptionHtml"
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
<script>
|
||||||
|
import { GlTab, GlTabs } from '@gitlab/ui';
|
||||||
|
import DescriptionComponent from './description.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GlTab,
|
||||||
|
GlTabs,
|
||||||
|
DescriptionComponent,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<gl-tabs
|
||||||
|
content-class="gl-reset-line-height gl-mt-3"
|
||||||
|
class="gl-mt-n3"
|
||||||
|
data-testid="incident-tabs"
|
||||||
|
>
|
||||||
|
<gl-tab :title="__('Summary')">
|
||||||
|
<description-component v-bind="$attrs" />
|
||||||
|
</gl-tab>
|
||||||
|
</gl-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -45,7 +45,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="border-bottom gl-mb-6 gl-display-flex gl-justify-content-start">
|
<div class="gl-display-flex gl-justify-content-start">
|
||||||
<template v-for="(link, i) in pinnedLinks">
|
<template v-for="(link, i) in pinnedLinks">
|
||||||
<div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }">
|
<div v-if="link.url" :key="link.id" :class="{ 'gl-pr-3': needsPaddingClass(i) }">
|
||||||
<gl-button
|
<gl-button
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import issuableApp from './components/app.vue';
|
||||||
|
import incidentTabs from './components/incident_tabs.vue';
|
||||||
|
|
||||||
|
export default function initIssuableApp(issuableData = {}) {
|
||||||
|
return new Vue({
|
||||||
|
el: document.getElementById('js-issuable-app'),
|
||||||
|
components: {
|
||||||
|
issuableApp,
|
||||||
|
},
|
||||||
|
render(createElement) {
|
||||||
|
return createElement('issuable-app', {
|
||||||
|
props: {
|
||||||
|
...issuableData,
|
||||||
|
descriptionComponent: incidentTabs,
|
||||||
|
showTitleBorder: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import issuableApp from './components/app.vue';
|
import issuableApp from './components/app.vue';
|
||||||
import { parseIssuableData } from './utils/parse_data';
|
|
||||||
|
|
||||||
export default function initIssueableApp() {
|
export default function initIssuableApp(issuableData) {
|
||||||
return new Vue({
|
return new Vue({
|
||||||
el: document.getElementById('js-issuable-app'),
|
el: document.getElementById('js-issuable-app'),
|
||||||
components: {
|
components: {
|
||||||
|
@ -10,7 +9,7 @@ export default function initIssueableApp() {
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement('issuable-app', {
|
return createElement('issuable-app', {
|
||||||
props: parseIssuableData(),
|
props: issuableData,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -4,14 +4,23 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
|
||||||
import ZenMode from '~/zen_mode';
|
import ZenMode from '~/zen_mode';
|
||||||
import '~/notes/index';
|
import '~/notes/index';
|
||||||
import { store } from '~/notes/stores';
|
import { store } from '~/notes/stores';
|
||||||
import initIssueableApp from '~/issue_show';
|
import initIssueApp from '~/issue_show/issue';
|
||||||
|
import initIncidentApp from '~/issue_show/incident';
|
||||||
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
|
import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning';
|
||||||
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
|
import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace';
|
||||||
import initRelatedMergeRequestsApp from '~/related_merge_requests';
|
import initRelatedMergeRequestsApp from '~/related_merge_requests';
|
||||||
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
|
import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle';
|
||||||
|
import { parseIssuableData } from '~/issue_show/utils/parse_data';
|
||||||
|
|
||||||
export default function() {
|
export default function() {
|
||||||
initIssueableApp();
|
const { issueType, ...issuableData } = parseIssuableData();
|
||||||
|
|
||||||
|
if (issueType === 'incident') {
|
||||||
|
initIncidentApp(issuableData);
|
||||||
|
} else {
|
||||||
|
initIssueApp(issuableData);
|
||||||
|
}
|
||||||
|
|
||||||
initIssuableHeaderWarning(store);
|
initIssuableHeaderWarning(store);
|
||||||
initSentryErrorStackTraceApp();
|
initSentryErrorStackTraceApp();
|
||||||
initRelatedMergeRequestsApp();
|
initRelatedMergeRequestsApp();
|
||||||
|
|
|
@ -1033,3 +1033,9 @@ $mr-widget-min-height: 69px;
|
||||||
.diff-file-row.is-active {
|
.diff-file-row.is-active {
|
||||||
background-color: $gray-50;
|
background-color: $gray-50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.merge-request-container {
|
||||||
|
.flash-container {
|
||||||
|
@include gl-mb-4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
module MergedAtFilter
|
module MergedAtFilter
|
||||||
private
|
private
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
|
||||||
def by_merged_at(items)
|
def by_merged_at(items)
|
||||||
return items unless merged_after || merged_before
|
return items unless merged_after || merged_before
|
||||||
|
|
||||||
|
@ -11,11 +10,8 @@ module MergedAtFilter
|
||||||
mr_metrics_scope = mr_metrics_scope.merged_after(merged_after) if merged_after.present?
|
mr_metrics_scope = mr_metrics_scope.merged_after(merged_after) if merged_after.present?
|
||||||
mr_metrics_scope = mr_metrics_scope.merged_before(merged_before) if merged_before.present?
|
mr_metrics_scope = mr_metrics_scope.merged_before(merged_before) if merged_before.present?
|
||||||
|
|
||||||
scope = items.joins(:metrics).merge(mr_metrics_scope)
|
items.join_metrics.merge(mr_metrics_scope)
|
||||||
scope = target_project_id_filter_on_metrics(scope) if Feature.enabled?(:improved_mr_merged_at_queries, default_enabled: true)
|
|
||||||
scope
|
|
||||||
end
|
end
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
|
||||||
|
|
||||||
def merged_after
|
def merged_after
|
||||||
params[:merged_after]
|
params[:merged_after]
|
||||||
|
@ -24,10 +20,4 @@ module MergedAtFilter
|
||||||
def merged_before
|
def merged_before
|
||||||
params[:merged_before]
|
params[:merged_before]
|
||||||
end
|
end
|
||||||
|
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
|
||||||
def target_project_id_filter_on_metrics(scope)
|
|
||||||
scope.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
|
|
||||||
end
|
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,6 +37,10 @@ module Resolvers
|
||||||
argument :milestone_title, GraphQL::STRING_TYPE,
|
argument :milestone_title, GraphQL::STRING_TYPE,
|
||||||
required: false,
|
required: false,
|
||||||
description: 'Title of the milestone'
|
description: 'Title of the milestone'
|
||||||
|
argument :sort, Types::MergeRequestSortEnum,
|
||||||
|
description: 'Sort merge requests by this criteria',
|
||||||
|
required: false,
|
||||||
|
default_value: 'created_desc'
|
||||||
|
|
||||||
def self.single
|
def self.single
|
||||||
::Resolvers::MergeRequestResolver
|
::Resolvers::MergeRequestResolver
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
class MergeRequestSortEnum < IssuableSortEnum
|
||||||
|
graphql_name 'MergeRequestSort'
|
||||||
|
description 'Values for sorting merge requests'
|
||||||
|
|
||||||
|
value 'MERGED_AT_ASC', 'Merge time by ascending order', value: :merged_at_asc
|
||||||
|
value 'MERGED_AT_DESC', 'Merge time by descending order', value: :merged_at_desc
|
||||||
|
end
|
||||||
|
end
|
|
@ -327,7 +327,8 @@ module ApplicationSettingsHelper
|
||||||
:group_import_limit,
|
:group_import_limit,
|
||||||
:group_export_limit,
|
:group_export_limit,
|
||||||
:group_download_export_limit,
|
:group_download_export_limit,
|
||||||
:wiki_page_max_content_bytes
|
:wiki_page_max_content_bytes,
|
||||||
|
:container_registry_delete_tags_service_timeout
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ContainerRegistryHelper
|
||||||
|
def limit_delete_tags_service?
|
||||||
|
Feature.enabled?(:container_registry_expiration_policies_throttling) &&
|
||||||
|
ContainerRegistry::Client.supports_tag_delete?
|
||||||
|
end
|
||||||
|
end
|
|
@ -292,6 +292,7 @@ module IssuablesHelper
|
||||||
|
|
||||||
{
|
{
|
||||||
hasClosingMergeRequest: issuable.merge_requests_count(current_user) != 0,
|
hasClosingMergeRequest: issuable.merge_requests_count(current_user) != 0,
|
||||||
|
issueType: issuable.issue_type,
|
||||||
zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
|
zoomMeetingUrl: ZoomMeeting.canonical_meeting_url(issuable),
|
||||||
sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier # rubocop:disable CodeReuse/ActiveRecord
|
sentryIssueIdentifier: SentryIssue.find_by(issue: issuable)&.sentry_issue_identifier # rubocop:disable CodeReuse/ActiveRecord
|
||||||
}
|
}
|
||||||
|
|
|
@ -282,6 +282,9 @@ class ApplicationSetting < ApplicationRecord
|
||||||
|
|
||||||
validates :hashed_storage_enabled, inclusion: { in: [true], message: _("Hashed storage can't be disabled anymore for new projects") }
|
validates :hashed_storage_enabled, inclusion: { in: [true], message: _("Hashed storage can't be disabled anymore for new projects") }
|
||||||
|
|
||||||
|
validates :container_registry_delete_tags_service_timeout,
|
||||||
|
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||||
|
|
||||||
SUPPORTED_KEY_TYPES.each do |type|
|
SUPPORTED_KEY_TYPES.each do |type|
|
||||||
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
|
||||||
end
|
end
|
||||||
|
|
|
@ -163,7 +163,8 @@ module ApplicationSettingImplementation
|
||||||
user_default_external: false,
|
user_default_external: false,
|
||||||
user_default_internal_regex: nil,
|
user_default_internal_regex: nil,
|
||||||
user_show_add_ssh_key_message: true,
|
user_show_add_ssh_key_message: true,
|
||||||
wiki_page_max_content_bytes: 50.megabytes
|
wiki_page_max_content_bytes: 50.megabytes,
|
||||||
|
container_registry_delete_tags_service_timeout: 100
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Atlassian
|
||||||
|
class Identity < ApplicationRecord
|
||||||
|
self.table_name = 'atlassian_identities'
|
||||||
|
|
||||||
|
belongs_to :user
|
||||||
|
|
||||||
|
validates :extern_uid, presence: true, uniqueness: true
|
||||||
|
validates :user, presence: true, uniqueness: true
|
||||||
|
|
||||||
|
attr_encrypted :token,
|
||||||
|
mode: :per_attribute_iv,
|
||||||
|
key: Settings.attr_encrypted_db_key_base_truncated,
|
||||||
|
algorithm: 'aes-256-gcm',
|
||||||
|
encode: false,
|
||||||
|
encode_iv: false
|
||||||
|
|
||||||
|
attr_encrypted :refresh_token,
|
||||||
|
mode: :per_attribute_iv,
|
||||||
|
key: Settings.attr_encrypted_db_key_base_truncated,
|
||||||
|
algorithm: 'aes-256-gcm',
|
||||||
|
encode: false,
|
||||||
|
encode_iv: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -251,6 +251,15 @@ class MergeRequest < ApplicationRecord
|
||||||
joins(:notes).where(notes: { commit_id: sha })
|
joins(:notes).where(notes: { commit_id: sha })
|
||||||
end
|
end
|
||||||
scope :join_project, -> { joins(:target_project) }
|
scope :join_project, -> { joins(:target_project) }
|
||||||
|
scope :join_metrics, -> do
|
||||||
|
query = joins(:metrics)
|
||||||
|
|
||||||
|
if Feature.enabled?(:improved_mr_merged_at_queries, default_enabled: true)
|
||||||
|
query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
|
||||||
|
end
|
||||||
|
|
||||||
|
query
|
||||||
|
end
|
||||||
scope :references_project, -> { references(:target_project) }
|
scope :references_project, -> { references(:target_project) }
|
||||||
scope :with_api_entity_associations, -> {
|
scope :with_api_entity_associations, -> {
|
||||||
preload_routables
|
preload_routables
|
||||||
|
@ -264,6 +273,14 @@ class MergeRequest < ApplicationRecord
|
||||||
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
|
where("target_branch LIKE ?", ApplicationRecord.sanitize_sql_like(wildcard_branch_name).tr('*', '%'))
|
||||||
end
|
end
|
||||||
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
|
scope :by_target_branch, ->(branch_name) { where(target_branch: branch_name) }
|
||||||
|
scope :order_merged_at, ->(direction) do
|
||||||
|
query = join_metrics.order(Gitlab::Database.nulls_last_order('merge_request_metrics.merged_at', direction))
|
||||||
|
|
||||||
|
# Add `merge_request_metrics.merged_at` to the `SELECT` in order to make the keyset pagination work.
|
||||||
|
query.select(*query.arel.projections, MergeRequest::Metrics.arel_table[:merged_at].as('"merge_request_metrics.merged_at"'))
|
||||||
|
end
|
||||||
|
scope :order_merged_at_asc, -> { order_merged_at('ASC') }
|
||||||
|
scope :order_merged_at_desc, -> { order_merged_at('DESC') }
|
||||||
scope :preload_source_project, -> { preload(:source_project) }
|
scope :preload_source_project, -> { preload(:source_project) }
|
||||||
scope :preload_target_project, -> { preload(:target_project) }
|
scope :preload_target_project, -> { preload(:target_project) }
|
||||||
scope :preload_routables, -> do
|
scope :preload_routables, -> do
|
||||||
|
@ -320,6 +337,15 @@ class MergeRequest < ApplicationRecord
|
||||||
.pluck(:target_branch)
|
.pluck(:target_branch)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.sort_by_attribute(method, excluded_labels: [])
|
||||||
|
case method.to_s
|
||||||
|
when 'merged_at', 'merged_at_asc' then order_merged_at_asc.with_order_id_desc
|
||||||
|
when 'merged_at_desc' then order_merged_at_desc.with_order_id_desc
|
||||||
|
else
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def rebase_in_progress?
|
def rebase_in_progress?
|
||||||
rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)
|
rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)
|
||||||
end
|
end
|
||||||
|
|
|
@ -351,10 +351,10 @@ class Service < ApplicationRecord
|
||||||
{ success: result.present?, result: result }
|
{ success: result.present?, result: result }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Disable test for instance-level services.
|
# Disable test for instance-level and group-level services.
|
||||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
|
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
|
||||||
def can_test?
|
def can_test?
|
||||||
!instance?
|
!instance? && !group_id
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a hash of the properties that have been assigned a new value since last save,
|
# Returns a hash of the properties that have been assigned a new value since last save,
|
||||||
|
|
|
@ -345,6 +345,10 @@ class Snippet < ApplicationRecord
|
||||||
repository.ls_files(ref)
|
repository.ls_files(ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def multiple_files?
|
||||||
|
list_files(repository.root_ref).size > 1
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
# Searches for snippets with a matching title, description or file name.
|
# Searches for snippets with a matching title, description or file name.
|
||||||
#
|
#
|
||||||
|
|
|
@ -181,6 +181,7 @@ class User < ApplicationRecord
|
||||||
has_one :user_detail
|
has_one :user_detail
|
||||||
has_one :user_highest_role
|
has_one :user_highest_role
|
||||||
has_one :user_canonical_email
|
has_one :user_canonical_email
|
||||||
|
has_one :atlassian_identity, class_name: 'Atlassian::Identity'
|
||||||
|
|
||||||
has_many :reviews, foreign_key: :author_id, inverse_of: :author
|
has_many :reviews, foreign_key: :author_id, inverse_of: :author
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
def scan_line!(line)
|
def scan_line!(line)
|
||||||
result = line.scan(/^(.*)=(.*)$/).last
|
result = line.scan(/^(.*?)=(.*)$/).last
|
||||||
|
|
||||||
raise ParserError, 'Invalid Format' if result.nil?
|
raise ParserError, 'Invalid Format' if result.nil?
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,11 @@ module Projects
|
||||||
module Gitlab
|
module Gitlab
|
||||||
class DeleteTagsService
|
class DeleteTagsService
|
||||||
include BaseServiceUtility
|
include BaseServiceUtility
|
||||||
|
include ::Gitlab::Utils::StrongMemoize
|
||||||
|
|
||||||
|
DISABLED_TIMEOUTS = [nil, 0].freeze
|
||||||
|
|
||||||
|
TimeoutError = Class.new(StandardError)
|
||||||
|
|
||||||
def initialize(container_repository, tag_names)
|
def initialize(container_repository, tag_names)
|
||||||
@container_repository = container_repository
|
@container_repository = container_repository
|
||||||
|
@ -17,12 +22,42 @@ module Projects
|
||||||
def execute
|
def execute
|
||||||
return success(deleted: []) if @tag_names.empty?
|
return success(deleted: []) if @tag_names.empty?
|
||||||
|
|
||||||
|
delete_tags
|
||||||
|
rescue TimeoutError => e
|
||||||
|
::Gitlab::ErrorTracking.track_exception(e, tags_count: @tag_names&.size, container_repository_id: @container_repository&.id)
|
||||||
|
error('timeout while deleting tags')
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def delete_tags
|
||||||
|
start_time = Time.zone.now
|
||||||
|
|
||||||
deleted_tags = @tag_names.select do |name|
|
deleted_tags = @tag_names.select do |name|
|
||||||
|
raise TimeoutError if timeout?(start_time)
|
||||||
|
|
||||||
@container_repository.delete_tag_by_name(name)
|
@container_repository.delete_tag_by_name(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags')
|
deleted_tags.any? ? success(deleted: deleted_tags) : error('could not delete tags')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def timeout?(start_time)
|
||||||
|
return false unless throttling_enabled?
|
||||||
|
return false if service_timeout.in?(DISABLED_TIMEOUTS)
|
||||||
|
|
||||||
|
(Time.zone.now - start_time) > service_timeout
|
||||||
|
end
|
||||||
|
|
||||||
|
def throttling_enabled?
|
||||||
|
strong_memoize(:feature_flag) do
|
||||||
|
Feature.enabled?(:container_registry_expiration_policies_throttling)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def service_timeout
|
||||||
|
::Gitlab::CurrentSettings.current_application_settings.container_registry_delete_tags_service_timeout
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,7 @@ module Projects
|
||||||
# This is a hack as the registry doesn't support deleting individual
|
# This is a hack as the registry doesn't support deleting individual
|
||||||
# tags. This code effectively pushes a dummy image and assigns the tag to it.
|
# tags. This code effectively pushes a dummy image and assigns the tag to it.
|
||||||
# This way when the tag is deleted only the dummy image is affected.
|
# This way when the tag is deleted only the dummy image is affected.
|
||||||
# This is used to preverse compatibility with third-party registries that
|
# This is used to preserve compatibility with third-party registries that
|
||||||
# don't support fast delete.
|
# don't support fast delete.
|
||||||
# See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
|
# See https://gitlab.com/gitlab-org/gitlab/issues/15737 for a discussion
|
||||||
def execute
|
def execute
|
||||||
|
|
|
@ -14,5 +14,11 @@
|
||||||
.form-text.text-muted
|
.form-text.text-muted
|
||||||
= _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.")
|
= _("Existing projects will be able to use expiration policies. Avoid enabling this if an external Container Registry is being used, as there is a performance risk if many images exist on one project.")
|
||||||
= link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries')
|
= link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'use-with-external-container-registries')
|
||||||
|
- if limit_delete_tags_service?
|
||||||
|
.form-group
|
||||||
|
= f.label :container_registry_delete_tags_service_timeout, _('Cleanup policy maximum processing time (seconds)'), class: 'label-bold'
|
||||||
|
= f.number_field :container_registry_delete_tags_service_timeout, min: 0, class: 'form-control'
|
||||||
|
.form-text.text-muted
|
||||||
|
= _("Tags are deleted until the timeout is reached. Any remaining tags are included the next time the policy runs. To remove the time limit, set it to 0.")
|
||||||
|
|
||||||
= f.submit 'Save changes', class: "btn btn-success"
|
= f.submit 'Save changes', class: "btn btn-success"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- @gfm_form = true
|
- @gfm_form = true
|
||||||
- @content_class = "limit-container-width" unless fluid_layout
|
- @content_class = "merge-request-container#{' limit-container-width' unless fluid_layout}"
|
||||||
- add_to_breadcrumbs _("Merge Requests"), project_merge_requests_path(@project)
|
- add_to_breadcrumbs _("Merge Requests"), project_merge_requests_path(@project)
|
||||||
- breadcrumb_title @merge_request.to_reference
|
- breadcrumb_title @merge_request.to_reference
|
||||||
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge Requests")
|
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge Requests")
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
.col-sm-12
|
.col-sm-12
|
||||||
= f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: s_('Wiki|Page title')
|
= f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: s_('Wiki|Page title')
|
||||||
%span.d-inline-block.mw-100.gl-mt-2
|
%span.d-inline-block.mw-100.gl-mt-2
|
||||||
= icon('lightbulb-o')
|
= sprite_icon('bulb', size: 12, css_class: 'gl-mr-n1')
|
||||||
- if @page.persisted?
|
- if @page.persisted?
|
||||||
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
|
= s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.")
|
||||||
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'),
|
= link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'),
|
||||||
|
|
|
@ -17,13 +17,13 @@
|
||||||
= sprite_icon('pencil')
|
= sprite_icon('pencil')
|
||||||
- elsif current_user
|
- elsif current_user
|
||||||
- if @user.abuse_report
|
- if @user.abuse_report
|
||||||
%button{ class: link_classes + 'btn btn-danger mr-1', title: s_('UserProfile|Already reported for abuse'),
|
%button{ class: link_classes + 'btn btn-danger', title: s_('UserProfile|Already reported for abuse'),
|
||||||
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }
|
data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } }>
|
||||||
= icon('exclamation-circle')
|
= sprite_icon('error')
|
||||||
- else
|
- else
|
||||||
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: link_classes + 'btn',
|
= link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: link_classes + 'btn',
|
||||||
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
|
||||||
= icon('exclamation-circle')
|
= sprite_icon('error')
|
||||||
- if can?(current_user, :read_user_profile, @user)
|
- if can?(current_user, :read_user_profile, @user)
|
||||||
= link_to user_path(@user, rss_url_options), class: link_classes + 'btn btn-svg btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
|
= link_to user_path(@user, rss_url_options), class: link_classes + 'btn btn-svg btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do
|
||||||
= sprite_icon('rss', css_class: 'qa-rss-icon')
|
= sprite_icon('rss', css_class: 'qa-rss-icon')
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add timeout support in the delete tags service for the GitLab Registry
|
||||||
|
merge_request: 36319
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Replace fa-exclamation-circle and fa-lightbulb-o with GitLab SVG icons
|
||||||
|
merge_request: 40857
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Bug fix GraphQL file uploads accepting non-file input
|
||||||
|
merge_request: 39763
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Migrating buttons and classes to match GitLab UI
|
||||||
|
merge_request: 40409
|
||||||
|
author:
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add MergeRequest sort options to GraphQL API
|
||||||
|
merge_request: 40138
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add Flash spacing on merge request show page
|
||||||
|
merge_request: 39903
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add Atlassian Identity to store identity/credentials
|
||||||
|
merge_request: 40176
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix RegExp for dotenv report artifact
|
||||||
|
merge_request: 38562
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add Summary tab for incident issues
|
||||||
|
merge_request: 39822
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,7 @@
|
||||||
|
---
|
||||||
|
name: container_registry_expiration_policies_throttling
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36319
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/238190
|
||||||
|
group: group::package
|
||||||
|
type: development
|
||||||
|
default_enabled: false
|
|
@ -0,0 +1,19 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddContainerRegistryDeleteTagsServiceTimeoutToApplicationSettings < ActiveRecord::Migration[6.0]
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_column(
|
||||||
|
:application_settings,
|
||||||
|
:container_registry_delete_tags_service_timeout,
|
||||||
|
:integer,
|
||||||
|
default: 250,
|
||||||
|
null: false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column(:application_settings, :container_registry_delete_tags_service_timeout)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateAtlassianIdentities < ActiveRecord::Migration[6.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
unless table_exists?(:atlassian_identities)
|
||||||
|
with_lock_retries do
|
||||||
|
create_table :atlassian_identities, id: false do |t|
|
||||||
|
t.references :user, index: false, foreign_key: { on_delete: :cascade }, null: false, primary_key: true
|
||||||
|
t.timestamps_with_timezone
|
||||||
|
t.datetime_with_timezone :expires_at
|
||||||
|
t.text :extern_uid, null: false, index: { unique: true }
|
||||||
|
t.binary :encrypted_token
|
||||||
|
t.binary :encrypted_token_iv
|
||||||
|
t.binary :encrypted_refresh_token
|
||||||
|
t.binary :encrypted_refresh_token_iv
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
add_text_limit :atlassian_identities, :extern_uid, 255
|
||||||
|
|
||||||
|
add_check_constraint :atlassian_identities, 'octet_length(encrypted_token) <= 2048', 'atlassian_identities_token_length_constraint'
|
||||||
|
add_check_constraint :atlassian_identities, 'octet_length(encrypted_token_iv) <= 12', 'atlassian_identities_token_iv_length_constraint'
|
||||||
|
add_check_constraint :atlassian_identities, 'octet_length(encrypted_refresh_token) <= 512', 'atlassian_identities_refresh_token_length_constraint'
|
||||||
|
add_check_constraint :atlassian_identities, 'octet_length(encrypted_refresh_token_iv) <= 12', 'atlassian_identities_refresh_token_iv_length_constraint'
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
with_lock_retries do
|
||||||
|
drop_table :atlassian_identities
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
3d49c22b718c5b4af0a7372584fe12ab730e1ffca501c7f582f7d01200708eb1
|
|
@ -0,0 +1 @@
|
||||||
|
d92cdef33a892fdd1761d9491bc8e4c782e9db348d4a6848a1470e99e644fbfd
|
|
@ -9266,6 +9266,7 @@ CREATE TABLE public.application_settings (
|
||||||
wiki_page_max_content_bytes bigint DEFAULT 52428800 NOT NULL,
|
wiki_page_max_content_bytes bigint DEFAULT 52428800 NOT NULL,
|
||||||
elasticsearch_indexed_file_size_limit_kb integer DEFAULT 1024 NOT NULL,
|
elasticsearch_indexed_file_size_limit_kb integer DEFAULT 1024 NOT NULL,
|
||||||
enforce_namespace_storage_limit boolean DEFAULT false NOT NULL,
|
enforce_namespace_storage_limit boolean DEFAULT false NOT NULL,
|
||||||
|
container_registry_delete_tags_service_timeout integer DEFAULT 250 NOT NULL,
|
||||||
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
|
CONSTRAINT check_51700b31b5 CHECK ((char_length(default_branch_name) <= 255)),
|
||||||
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
|
CONSTRAINT check_9c6c447a13 CHECK ((char_length(maintenance_mode_message) <= 255)),
|
||||||
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
|
CONSTRAINT check_d03919528d CHECK ((char_length(container_registry_vendor) <= 255)),
|
||||||
|
@ -9479,6 +9480,32 @@ CREATE TABLE public.ar_internal_metadata (
|
||||||
updated_at timestamp(6) without time zone NOT NULL
|
updated_at timestamp(6) without time zone NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE public.atlassian_identities (
|
||||||
|
user_id bigint NOT NULL,
|
||||||
|
created_at timestamp with time zone NOT NULL,
|
||||||
|
updated_at timestamp with time zone NOT NULL,
|
||||||
|
expires_at timestamp with time zone,
|
||||||
|
extern_uid text NOT NULL,
|
||||||
|
encrypted_token bytea,
|
||||||
|
encrypted_token_iv bytea,
|
||||||
|
encrypted_refresh_token bytea,
|
||||||
|
encrypted_refresh_token_iv bytea,
|
||||||
|
CONSTRAINT atlassian_identities_refresh_token_iv_length_constraint CHECK ((octet_length(encrypted_refresh_token_iv) <= 12)),
|
||||||
|
CONSTRAINT atlassian_identities_refresh_token_length_constraint CHECK ((octet_length(encrypted_refresh_token) <= 512)),
|
||||||
|
CONSTRAINT atlassian_identities_token_iv_length_constraint CHECK ((octet_length(encrypted_token_iv) <= 12)),
|
||||||
|
CONSTRAINT atlassian_identities_token_length_constraint CHECK ((octet_length(encrypted_token) <= 2048)),
|
||||||
|
CONSTRAINT check_32f5779763 CHECK ((char_length(extern_uid) <= 255))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE SEQUENCE public.atlassian_identities_user_id_seq
|
||||||
|
START WITH 1
|
||||||
|
INCREMENT BY 1
|
||||||
|
NO MINVALUE
|
||||||
|
NO MAXVALUE
|
||||||
|
CACHE 1;
|
||||||
|
|
||||||
|
ALTER SEQUENCE public.atlassian_identities_user_id_seq OWNED BY public.atlassian_identities.user_id;
|
||||||
|
|
||||||
CREATE TABLE public.audit_events (
|
CREATE TABLE public.audit_events (
|
||||||
id integer NOT NULL,
|
id integer NOT NULL,
|
||||||
author_id integer NOT NULL,
|
author_id integer NOT NULL,
|
||||||
|
@ -16880,6 +16907,8 @@ ALTER TABLE ONLY public.approver_groups ALTER COLUMN id SET DEFAULT nextval('pub
|
||||||
|
|
||||||
ALTER TABLE ONLY public.approvers ALTER COLUMN id SET DEFAULT nextval('public.approvers_id_seq'::regclass);
|
ALTER TABLE ONLY public.approvers ALTER COLUMN id SET DEFAULT nextval('public.approvers_id_seq'::regclass);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.atlassian_identities ALTER COLUMN user_id SET DEFAULT nextval('public.atlassian_identities_user_id_seq'::regclass);
|
||||||
|
|
||||||
ALTER TABLE ONLY public.audit_events ALTER COLUMN id SET DEFAULT nextval('public.audit_events_id_seq'::regclass);
|
ALTER TABLE ONLY public.audit_events ALTER COLUMN id SET DEFAULT nextval('public.audit_events_id_seq'::regclass);
|
||||||
|
|
||||||
ALTER TABLE ONLY public.award_emoji ALTER COLUMN id SET DEFAULT nextval('public.award_emoji_id_seq'::regclass);
|
ALTER TABLE ONLY public.award_emoji ALTER COLUMN id SET DEFAULT nextval('public.award_emoji_id_seq'::regclass);
|
||||||
|
@ -17800,6 +17829,9 @@ ALTER TABLE ONLY public.approvers
|
||||||
ALTER TABLE ONLY public.ar_internal_metadata
|
ALTER TABLE ONLY public.ar_internal_metadata
|
||||||
ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key);
|
ADD CONSTRAINT ar_internal_metadata_pkey PRIMARY KEY (key);
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.atlassian_identities
|
||||||
|
ADD CONSTRAINT atlassian_identities_pkey PRIMARY KEY (user_id);
|
||||||
|
|
||||||
ALTER TABLE ONLY public.audit_events_part_5fc467ac26
|
ALTER TABLE ONLY public.audit_events_part_5fc467ac26
|
||||||
ADD CONSTRAINT audit_events_part_5fc467ac26_pkey PRIMARY KEY (id, created_at);
|
ADD CONSTRAINT audit_events_part_5fc467ac26_pkey PRIMARY KEY (id, created_at);
|
||||||
|
|
||||||
|
@ -19237,6 +19269,8 @@ CREATE INDEX index_approvers_on_target_id_and_target_type ON public.approvers US
|
||||||
|
|
||||||
CREATE INDEX index_approvers_on_user_id ON public.approvers USING btree (user_id);
|
CREATE INDEX index_approvers_on_user_id ON public.approvers USING btree (user_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX index_atlassian_identities_on_extern_uid ON public.atlassian_identities USING btree (extern_uid);
|
||||||
|
|
||||||
CREATE INDEX index_audit_events_on_entity_id_entity_type_id_desc_author_id ON public.audit_events USING btree (entity_id, entity_type, id DESC, author_id);
|
CREATE INDEX index_audit_events_on_entity_id_entity_type_id_desc_author_id ON public.audit_events USING btree (entity_id, entity_type, id DESC, author_id);
|
||||||
|
|
||||||
CREATE INDEX index_award_emoji_on_awardable_type_and_awardable_id ON public.award_emoji USING btree (awardable_type, awardable_id);
|
CREATE INDEX index_award_emoji_on_awardable_type_and_awardable_id ON public.award_emoji USING btree (awardable_type, awardable_id);
|
||||||
|
@ -23097,6 +23131,9 @@ ALTER TABLE ONLY public.resource_weight_events
|
||||||
ALTER TABLE ONLY public.design_management_designs
|
ALTER TABLE ONLY public.design_management_designs
|
||||||
ADD CONSTRAINT fk_rails_bfe283ec3c FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
|
ADD CONSTRAINT fk_rails_bfe283ec3c FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE ONLY public.atlassian_identities
|
||||||
|
ADD CONSTRAINT fk_rails_c02928bc18 FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
ALTER TABLE ONLY public.serverless_domain_cluster
|
ALTER TABLE ONLY public.serverless_domain_cluster
|
||||||
ADD CONSTRAINT fk_rails_c09009dee1 FOREIGN KEY (pages_domain_id) REFERENCES public.pages_domains(id) ON DELETE CASCADE;
|
ADD CONSTRAINT fk_rails_c09009dee1 FOREIGN KEY (pages_domain_id) REFERENCES public.pages_domains(id) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
|
|
@ -9520,6 +9520,71 @@ type MergeRequestSetWipPayload {
|
||||||
mergeRequest: MergeRequest
|
mergeRequest: MergeRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Values for sorting merge requests
|
||||||
|
"""
|
||||||
|
enum MergeRequestSort {
|
||||||
|
"""
|
||||||
|
Label priority by ascending order
|
||||||
|
"""
|
||||||
|
LABEL_PRIORITY_ASC
|
||||||
|
|
||||||
|
"""
|
||||||
|
Label priority by descending order
|
||||||
|
"""
|
||||||
|
LABEL_PRIORITY_DESC
|
||||||
|
|
||||||
|
"""
|
||||||
|
Merge time by ascending order
|
||||||
|
"""
|
||||||
|
MERGED_AT_ASC
|
||||||
|
|
||||||
|
"""
|
||||||
|
Merge time by descending order
|
||||||
|
"""
|
||||||
|
MERGED_AT_DESC
|
||||||
|
|
||||||
|
"""
|
||||||
|
Milestone due date by ascending order
|
||||||
|
"""
|
||||||
|
MILESTONE_DUE_ASC
|
||||||
|
|
||||||
|
"""
|
||||||
|
Milestone due date by descending order
|
||||||
|
"""
|
||||||
|
MILESTONE_DUE_DESC
|
||||||
|
|
||||||
|
"""
|
||||||
|
Priority by ascending order
|
||||||
|
"""
|
||||||
|
PRIORITY_ASC
|
||||||
|
|
||||||
|
"""
|
||||||
|
Priority by descending order
|
||||||
|
"""
|
||||||
|
PRIORITY_DESC
|
||||||
|
|
||||||
|
"""
|
||||||
|
Created at ascending order
|
||||||
|
"""
|
||||||
|
created_asc
|
||||||
|
|
||||||
|
"""
|
||||||
|
Created at descending order
|
||||||
|
"""
|
||||||
|
created_desc
|
||||||
|
|
||||||
|
"""
|
||||||
|
Updated at ascending order
|
||||||
|
"""
|
||||||
|
updated_asc
|
||||||
|
|
||||||
|
"""
|
||||||
|
Updated at descending order
|
||||||
|
"""
|
||||||
|
updated_desc
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
State of a GitLab merge request
|
State of a GitLab merge request
|
||||||
"""
|
"""
|
||||||
|
@ -11741,6 +11806,11 @@ type Project {
|
||||||
"""
|
"""
|
||||||
milestoneTitle: String
|
milestoneTitle: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Sort merge requests by this criteria
|
||||||
|
"""
|
||||||
|
sort: MergeRequestSort = created_desc
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Array of source branch names. All resolved merge requests will have one of these branches as their source.
|
Array of source branch names. All resolved merge requests will have one of these branches as their source.
|
||||||
"""
|
"""
|
||||||
|
@ -16808,6 +16878,11 @@ type User {
|
||||||
"""
|
"""
|
||||||
projectPath: String
|
projectPath: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Sort merge requests by this criteria
|
||||||
|
"""
|
||||||
|
sort: MergeRequestSort = created_desc
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Array of source branch names. All resolved merge requests will have one of these branches as their source.
|
Array of source branch names. All resolved merge requests will have one of these branches as their source.
|
||||||
"""
|
"""
|
||||||
|
@ -16883,6 +16958,11 @@ type User {
|
||||||
"""
|
"""
|
||||||
projectPath: String
|
projectPath: String
|
||||||
|
|
||||||
|
"""
|
||||||
|
Sort merge requests by this criteria
|
||||||
|
"""
|
||||||
|
sort: MergeRequestSort = created_desc
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Array of source branch names. All resolved merge requests will have one of these branches as their source.
|
Array of source branch names. All resolved merge requests will have one of these branches as their source.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -26639,6 +26639,89 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "MergeRequestSort",
|
||||||
|
"description": "Values for sorting merge requests",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": [
|
||||||
|
{
|
||||||
|
"name": "updated_desc",
|
||||||
|
"description": "Updated at descending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_asc",
|
||||||
|
"description": "Updated at ascending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_desc",
|
||||||
|
"description": "Created at descending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_asc",
|
||||||
|
"description": "Created at ascending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PRIORITY_ASC",
|
||||||
|
"description": "Priority by ascending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PRIORITY_DESC",
|
||||||
|
"description": "Priority by descending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LABEL_PRIORITY_ASC",
|
||||||
|
"description": "Label priority by ascending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "LABEL_PRIORITY_DESC",
|
||||||
|
"description": "Label priority by descending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MILESTONE_DUE_ASC",
|
||||||
|
"description": "Milestone due date by ascending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MILESTONE_DUE_DESC",
|
||||||
|
"description": "Milestone due date by descending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MERGED_AT_ASC",
|
||||||
|
"description": "Merge time by ascending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MERGED_AT_DESC",
|
||||||
|
"description": "Merge time by descending order",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "ENUM",
|
"kind": "ENUM",
|
||||||
"name": "MergeRequestState",
|
"name": "MergeRequestState",
|
||||||
|
@ -34771,6 +34854,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sort",
|
||||||
|
"description": "Sort merge requests by this criteria",
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "MergeRequestSort",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": "created_desc"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "assigneeUsername",
|
"name": "assigneeUsername",
|
||||||
"description": "Username of the assignee",
|
"description": "Username of the assignee",
|
||||||
|
@ -49461,6 +49554,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sort",
|
||||||
|
"description": "Sort merge requests by this criteria",
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "MergeRequestSort",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": "created_desc"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "projectPath",
|
"name": "projectPath",
|
||||||
"description": "The full-path of the project the authored merge requests should be in. Incompatible with projectId.",
|
"description": "The full-path of the project the authored merge requests should be in. Incompatible with projectId.",
|
||||||
|
@ -49646,6 +49749,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"defaultValue": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "sort",
|
||||||
|
"description": "Sort merge requests by this criteria",
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "MergeRequestSort",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"defaultValue": "created_desc"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "projectPath",
|
"name": "projectPath",
|
||||||
"description": "The full-path of the project the authored merge requests should be in. Incompatible with projectId.",
|
"description": "The full-path of the project the authored merge requests should be in. Incompatible with projectId.",
|
||||||
|
|
|
@ -533,6 +533,11 @@ The cleanup policy:
|
||||||
1. Excludes from the list any tags matching the `name_regex_keep` value (tags to preserve).
|
1. Excludes from the list any tags matching the `name_regex_keep` value (tags to preserve).
|
||||||
1. Finally, the remaining tags in the list are deleted from the Container Registry.
|
1. Finally, the remaining tags in the list are deleted from the Container Registry.
|
||||||
|
|
||||||
|
CAUTION: **Warning:**
|
||||||
|
On GitLab.com, the execution time for the cleanup policy is limited, and some of the tags may remain in
|
||||||
|
the Container Registry after the policy runs. The next time the policy runs, the remaining tags are included,
|
||||||
|
so it may take multiple runs for all tags to be deleted.
|
||||||
|
|
||||||
### Create a cleanup policy
|
### Create a cleanup policy
|
||||||
|
|
||||||
You can create a cleanup policy in [the API](#use-the-cleanup-policy-api) or the UI.
|
You can create a cleanup policy in [the API](#use-the-cleanup-policy-api) or the UI.
|
||||||
|
|
|
@ -537,6 +537,10 @@ module API
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def with_api_params(&block)
|
||||||
|
yield({ api: true, request: request })
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def project_finder_params_visibility_ce
|
def project_finder_params_visibility_ce
|
||||||
|
|
|
@ -27,6 +27,20 @@ module API
|
||||||
exactly_one_of :files, :content
|
exactly_one_of :files, :content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
params :update_file_params do |options|
|
||||||
|
optional :files, type: Array, desc: 'An array of files to update' do
|
||||||
|
requires :action, type: String,
|
||||||
|
values: SnippetInputAction::ACTIONS.map(&:to_s),
|
||||||
|
desc: "The type of action to perform on the file, must be one of: #{SnippetInputAction::ACTIONS.join(", ")}"
|
||||||
|
optional :content, type: String, desc: 'The content of a snippet'
|
||||||
|
optional :file_path, file_path: true, type: String, desc: 'The file path of a snippet file'
|
||||||
|
optional :previous_path, file_path: true, type: String, desc: 'The previous path of a snippet file'
|
||||||
|
end
|
||||||
|
|
||||||
|
mutually_exclusive :files, :content
|
||||||
|
mutually_exclusive :files, :file_name
|
||||||
|
end
|
||||||
|
|
||||||
def content_for(snippet)
|
def content_for(snippet)
|
||||||
if snippet.empty_repo?
|
if snippet.empty_repo?
|
||||||
env['api.format'] = :txt
|
env['api.format'] = :txt
|
||||||
|
@ -53,11 +67,31 @@ module API
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_file_args(args)
|
def process_create_params(args)
|
||||||
|
with_api_params do |api_params|
|
||||||
args[:snippet_actions] = args.delete(:files)&.map do |file|
|
args[:snippet_actions] = args.delete(:files)&.map do |file|
|
||||||
file[:action] = :create
|
file[:action] = :create
|
||||||
file.symbolize_keys
|
file.symbolize_keys
|
||||||
end
|
end
|
||||||
|
|
||||||
|
args.merge(api_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_update_params(args)
|
||||||
|
with_api_params do |api_params|
|
||||||
|
args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys)
|
||||||
|
|
||||||
|
args.merge(api_params)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_params_for_multiple_files(snippet)
|
||||||
|
return unless params[:content] || params[:file_name]
|
||||||
|
|
||||||
|
if Feature.enabled?(:snippet_multiple_files, current_user) && snippet.multiple_files?
|
||||||
|
render_api_error!({ error: _('To update Snippets with multiple files, you must use the `files` parameter') }, 400)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -64,12 +64,8 @@ module API
|
||||||
end
|
end
|
||||||
post ":id/snippets" do
|
post ":id/snippets" do
|
||||||
authorize! :create_snippet, user_project
|
authorize! :create_snippet, user_project
|
||||||
snippet_params = declared_params(include_missing: false).tap do |create_args|
|
|
||||||
create_args[:request] = request
|
|
||||||
create_args[:api] = true
|
|
||||||
|
|
||||||
process_file_args(create_args)
|
snippet_params = process_create_params(declared_params(include_missing: false))
|
||||||
end
|
|
||||||
|
|
||||||
service_response = ::Snippets::CreateService.new(user_project, current_user, snippet_params).execute
|
service_response = ::Snippets::CreateService.new(user_project, current_user, snippet_params).execute
|
||||||
snippet = service_response.payload[:snippet]
|
snippet = service_response.payload[:snippet]
|
||||||
|
|
|
@ -76,12 +76,7 @@ module API
|
||||||
post do
|
post do
|
||||||
authorize! :create_snippet
|
authorize! :create_snippet
|
||||||
|
|
||||||
attrs = declared_params(include_missing: false).tap do |create_args|
|
attrs = process_create_params(declared_params(include_missing: false))
|
||||||
create_args[:request] = request
|
|
||||||
create_args[:api] = true
|
|
||||||
|
|
||||||
process_file_args(create_args)
|
|
||||||
end
|
|
||||||
|
|
||||||
service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute
|
service_response = ::Snippets::CreateService.new(nil, current_user, attrs).execute
|
||||||
snippet = service_response.payload[:snippet]
|
snippet = service_response.payload[:snippet]
|
||||||
|
@ -99,16 +94,20 @@ module API
|
||||||
detail 'This feature was introduced in GitLab 8.15.'
|
detail 'This feature was introduced in GitLab 8.15.'
|
||||||
success Entities::PersonalSnippet
|
success Entities::PersonalSnippet
|
||||||
end
|
end
|
||||||
|
|
||||||
params do
|
params do
|
||||||
requires :id, type: Integer, desc: 'The ID of a snippet'
|
requires :id, type: Integer, desc: 'The ID of a snippet'
|
||||||
optional :title, type: String, allow_blank: false, desc: 'The title of a snippet'
|
|
||||||
optional :file_name, type: String, desc: 'The name of a snippet file'
|
|
||||||
optional :content, type: String, allow_blank: false, desc: 'The content of a snippet'
|
optional :content, type: String, allow_blank: false, desc: 'The content of a snippet'
|
||||||
optional :description, type: String, desc: 'The description of a snippet'
|
optional :description, type: String, desc: 'The description of a snippet'
|
||||||
|
optional :file_name, type: String, desc: 'The name of a snippet file'
|
||||||
|
optional :title, type: String, allow_blank: false, desc: 'The title of a snippet'
|
||||||
optional :visibility, type: String,
|
optional :visibility, type: String,
|
||||||
values: Gitlab::VisibilityLevel.string_values,
|
values: Gitlab::VisibilityLevel.string_values,
|
||||||
desc: 'The visibility of the snippet'
|
desc: 'The visibility of the snippet'
|
||||||
at_least_one_of :title, :file_name, :content, :visibility
|
|
||||||
|
use :update_file_params
|
||||||
|
|
||||||
|
at_least_one_of :title, :file_name, :content, :files, :visibility
|
||||||
end
|
end
|
||||||
put ':id' do
|
put ':id' do
|
||||||
snippet = snippets_for_current_user.find_by_id(params.delete(:id))
|
snippet = snippets_for_current_user.find_by_id(params.delete(:id))
|
||||||
|
@ -116,8 +115,12 @@ module API
|
||||||
|
|
||||||
authorize! :update_snippet, snippet
|
authorize! :update_snippet, snippet
|
||||||
|
|
||||||
attrs = declared_params(include_missing: false).merge(request: request, api: true)
|
validate_params_for_multiple_files(snippet)
|
||||||
|
|
||||||
|
attrs = process_update_params(declared_params(include_missing: false))
|
||||||
|
|
||||||
service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet)
|
service_response = ::Snippets::UpdateService.new(nil, current_user, attrs).execute(snippet)
|
||||||
|
|
||||||
snippet = service_response.payload[:snippet]
|
snippet = service_response.payload[:snippet]
|
||||||
|
|
||||||
if service_response.success?
|
if service_response.success?
|
||||||
|
|
|
@ -21,6 +21,17 @@ module ContainerRegistry
|
||||||
# Taken from: FaradayMiddleware::FollowRedirects
|
# Taken from: FaradayMiddleware::FollowRedirects
|
||||||
REDIRECT_CODES = Set.new [301, 302, 303, 307]
|
REDIRECT_CODES = Set.new [301, 302, 303, 307]
|
||||||
|
|
||||||
|
def self.supports_tag_delete?
|
||||||
|
registry_config = Gitlab.config.registry
|
||||||
|
return false unless registry_config.enabled && registry_config.api_url.present?
|
||||||
|
|
||||||
|
return true if ::Gitlab.com?
|
||||||
|
|
||||||
|
token = Auth::ContainerRegistryAuthenticationService.access_token([], [])
|
||||||
|
client = new(registry_config.api_url, token: token)
|
||||||
|
client.supports_tag_delete?
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(base_uri, options = {})
|
def initialize(base_uri, options = {})
|
||||||
@base_uri = base_uri
|
@base_uri = base_uri
|
||||||
@options = options
|
@options = options
|
||||||
|
|
|
@ -29,7 +29,7 @@ module Gitlab
|
||||||
def table_condition(order_info, value, operator)
|
def table_condition(order_info, value, operator)
|
||||||
if order_info.named_function
|
if order_info.named_function
|
||||||
target = order_info.named_function
|
target = order_info.named_function
|
||||||
value = value&.downcase if target&.name&.downcase == 'lower'
|
value = value&.downcase if target.respond_to?(:name) && target&.name&.downcase == 'lower'
|
||||||
else
|
else
|
||||||
target = arel_table[order_info.attribute_name]
|
target = arel_table[order_info.attribute_name]
|
||||||
end
|
end
|
||||||
|
|
|
@ -71,7 +71,22 @@ module Gitlab
|
||||||
def extract_nulls_last_order(order_value)
|
def extract_nulls_last_order(order_value)
|
||||||
tokens = order_value.downcase.split
|
tokens = order_value.downcase.split
|
||||||
|
|
||||||
[tokens.first, (tokens[1] == 'asc' ? :asc : :desc), nil]
|
column_reference = tokens.first
|
||||||
|
sort_direction = tokens[1] == 'asc' ? :asc : :desc
|
||||||
|
|
||||||
|
# Handles the case when the order value is coming from another table.
|
||||||
|
# Example: table_name.column_name
|
||||||
|
# Query the value using the fully qualified column name: pass table_name.column_name as the named_function
|
||||||
|
if fully_qualified_column_reference?(column_reference)
|
||||||
|
[column_reference, sort_direction, Arel.sql(column_reference)]
|
||||||
|
else
|
||||||
|
[column_reference, sort_direction, nil]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Example: table_name.column_name
|
||||||
|
def fully_qualified_column_reference?(attribute)
|
||||||
|
attribute.to_s.count('.') == 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_attribute_values(order_value)
|
def extract_attribute_values(order_value)
|
||||||
|
|
|
@ -2834,6 +2834,9 @@ msgstr ""
|
||||||
msgid "An error occurred while retrieving diff files"
|
msgid "An error occurred while retrieving diff files"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "An error occurred while retrieving projects."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "An error occurred while saving LDAP override status. Please try again."
|
msgid "An error occurred while saving LDAP override status. Please try again."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -5006,6 +5009,9 @@ msgstr ""
|
||||||
msgid "Cleanup policy for tags"
|
msgid "Cleanup policy for tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Cleanup policy maximum processing time (seconds)"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Clear"
|
msgid "Clear"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -8610,6 +8616,9 @@ msgstr ""
|
||||||
msgid "Diff limits"
|
msgid "Diff limits"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Diff view settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Difference between start date and now"
|
msgid "Difference between start date and now"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -24097,6 +24106,9 @@ msgstr ""
|
||||||
msgid "Tags"
|
msgid "Tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Tags are deleted until the timeout is reached. Any remaining tags are included the next time the policy runs. To remove the time limit, set it to 0."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Tags feed"
|
msgid "Tags feed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -25968,6 +25980,9 @@ msgstr ""
|
||||||
msgid "To unsubscribe from this issue, please paste the following link into your browser:"
|
msgid "To unsubscribe from this issue, please paste the following link into your browser:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "To update Snippets with multiple files, you must use the `files` parameter"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
|
msgid "To view all %{scannedResourcesCount} scanned URLs, please download the CSV file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -12,19 +12,21 @@ module QA
|
||||||
# implementation so that it's not included.
|
# implementation so that it's not included.
|
||||||
end
|
end
|
||||||
|
|
||||||
def stop(notification)
|
def stop(example_notification)
|
||||||
# Based on https://github.com/rspec/rspec-core/blob/main/lib/rspec/core/formatters/json_formatter.rb#L35
|
# Based on https://github.com/rspec/rspec-core/blob/main/lib/rspec/core/formatters/json_formatter.rb#L35
|
||||||
# But modified to include full details of multiple exceptions
|
# But modified to include full details of multiple exceptions and to provide output similar to
|
||||||
@output_hash[:examples] = notification.examples.map do |example|
|
# https://github.com/sj26/rspec_junit_formatter
|
||||||
format_example(example).tap do |hash|
|
@output_hash[:examples] = example_notification.notifications.map do |notification|
|
||||||
e = example.exception
|
format_example(notification.example).tap do |hash|
|
||||||
|
e = notification.example.exception
|
||||||
if e
|
if e
|
||||||
exceptions = e.respond_to?(:all_exceptions) ? e.all_exceptions : [e]
|
exceptions = e.respond_to?(:all_exceptions) ? e.all_exceptions : [e]
|
||||||
hash[:exceptions] = exceptions.map do |exception|
|
hash[:exceptions] = exceptions.map do |exception|
|
||||||
{
|
{
|
||||||
class: exception.class.name,
|
class: exception.class.name,
|
||||||
message: exception.message,
|
message: exception.message,
|
||||||
backtrace: exception.backtrace
|
message_lines: strip_ansi_codes(notification.message_lines),
|
||||||
|
backtrace: notification.formatted_backtrace
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -60,6 +62,12 @@ module QA
|
||||||
metadata[:shared_group_inclusion_backtrace].last.formatted_inclusion_location.split(':')
|
metadata[:shared_group_inclusion_backtrace].last.formatted_inclusion_location.split(':')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def strip_ansi_codes(strings)
|
||||||
|
# The code below is from https://github.com/piotrmurach/pastel/blob/master/lib/pastel/color.rb
|
||||||
|
modified = Array(strings).map { |string| string.dup.gsub(/\x1b\[{1,2}[0-9;:?]*m/m, '') }
|
||||||
|
modified.size == 1 ? modified[0] : modified
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
FactoryBot.define do
|
||||||
|
factory :atlassian_identity, class: 'Atlassian::Identity' do
|
||||||
|
extern_uid { generate(:username) }
|
||||||
|
user { create(:user) }
|
||||||
|
expires_at { 2.weeks.from_now }
|
||||||
|
token { SecureRandom.alphanumeric(1254) }
|
||||||
|
refresh_token { SecureRandom.alphanumeric(45) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -285,6 +285,55 @@ RSpec.describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_n
|
||||||
expect(current_settings.auto_devops_domain).to eq('domain.com')
|
expect(current_settings.auto_devops_domain).to eq('domain.com')
|
||||||
expect(page).to have_content "Application settings saved successfully"
|
expect(page).to have_content "Application settings saved successfully"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'Container Registry' do
|
||||||
|
context 'delete tags service execution timeout' do
|
||||||
|
let(:feature_flag_enabled) { true }
|
||||||
|
let(:client_support) { true }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_container_registry_config(enabled: true)
|
||||||
|
stub_feature_flags(container_registry_expiration_policies_throttling: feature_flag_enabled)
|
||||||
|
allow(ContainerRegistry::Client).to receive(:supports_tag_delete?).and_return(client_support)
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples 'not having service timeout settings' do
|
||||||
|
it 'lacks the timeout settings' do
|
||||||
|
visit ci_cd_admin_application_settings_path
|
||||||
|
|
||||||
|
expect(page).not_to have_content "Container Registry delete tags service execution timeout"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with feature flag enabled' do
|
||||||
|
context 'with client supporting tag delete' do
|
||||||
|
it 'changes the timeout' do
|
||||||
|
visit ci_cd_admin_application_settings_path
|
||||||
|
|
||||||
|
page.within('.as-registry') do
|
||||||
|
fill_in 'application_setting_container_registry_delete_tags_service_timeout', with: 400
|
||||||
|
click_button 'Save changes'
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(current_settings.container_registry_delete_tags_service_timeout).to eq(400)
|
||||||
|
expect(page).to have_content "Application settings saved successfully"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with client not supporting tag delete' do
|
||||||
|
let(:client_support) { false }
|
||||||
|
|
||||||
|
it_behaves_like 'not having service timeout settings'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with feature flag disabled' do
|
||||||
|
let(:feature_flag_enabled) { false }
|
||||||
|
|
||||||
|
it_behaves_like 'not having service timeout settings'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'Repository page' do
|
context 'Repository page' do
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Incident Detail', :js do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:project) { create(:project, :public) }
|
||||||
|
let(:incident) { create(:issue, project: project, author: user, issue_type: 'incident', description: 'hello') }
|
||||||
|
|
||||||
|
context 'when user displays the incident' do
|
||||||
|
before do
|
||||||
|
visit project_issue_path(project, incident)
|
||||||
|
wait_for_requests
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows the incident tabs' do
|
||||||
|
page.within('.issuable-details') do
|
||||||
|
incident_tabs = find('[data-testid="incident-tabs"]')
|
||||||
|
|
||||||
|
expect(find('h2')).to have_content(incident.title)
|
||||||
|
expect(incident_tabs).to have_content('Summary')
|
||||||
|
expect(incident_tabs).to have_content(incident.description)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -7,7 +7,7 @@ import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constant
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
localVue.use(Vuex);
|
localVue.use(Vuex);
|
||||||
|
|
||||||
describe('Diff settiings dropdown component', () => {
|
describe('Diff settings dropdown component', () => {
|
||||||
let vm;
|
let vm;
|
||||||
let actions;
|
let actions;
|
||||||
|
|
||||||
|
@ -61,50 +61,50 @@ describe('Diff settiings dropdown component', () => {
|
||||||
expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true, undefined);
|
expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets list button as active when renderTreeList is false', () => {
|
it('sets list button as selected when renderTreeList is false', () => {
|
||||||
createComponent(store => {
|
createComponent(store => {
|
||||||
Object.assign(store.state.diffs, {
|
Object.assign(store.state.diffs, {
|
||||||
renderTreeList: false,
|
renderTreeList: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(vm.find('.js-list-view').classes('active')).toBe(true);
|
expect(vm.find('.js-list-view').classes('selected')).toBe(true);
|
||||||
expect(vm.find('.js-tree-view').classes('active')).toBe(false);
|
expect(vm.find('.js-tree-view').classes('selected')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets tree button as active when renderTreeList is true', () => {
|
it('sets tree button as selected when renderTreeList is true', () => {
|
||||||
createComponent(store => {
|
createComponent(store => {
|
||||||
Object.assign(store.state.diffs, {
|
Object.assign(store.state.diffs, {
|
||||||
renderTreeList: true,
|
renderTreeList: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(vm.find('.js-list-view').classes('active')).toBe(false);
|
expect(vm.find('.js-list-view').classes('selected')).toBe(false);
|
||||||
expect(vm.find('.js-tree-view').classes('active')).toBe(true);
|
expect(vm.find('.js-tree-view').classes('selected')).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('compare changes', () => {
|
describe('compare changes', () => {
|
||||||
it('sets inline button as active', () => {
|
it('sets inline button as selected', () => {
|
||||||
createComponent(store => {
|
createComponent(store => {
|
||||||
Object.assign(store.state.diffs, {
|
Object.assign(store.state.diffs, {
|
||||||
diffViewType: INLINE_DIFF_VIEW_TYPE,
|
diffViewType: INLINE_DIFF_VIEW_TYPE,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(vm.find('.js-inline-diff-button').classes('active')).toBe(true);
|
expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(true);
|
||||||
expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(false);
|
expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets parallel button as active', () => {
|
it('sets parallel button as selected', () => {
|
||||||
createComponent(store => {
|
createComponent(store => {
|
||||||
Object.assign(store.state.diffs, {
|
Object.assign(store.state.diffs, {
|
||||||
diffViewType: PARALLEL_DIFF_VIEW_TYPE,
|
diffViewType: PARALLEL_DIFF_VIEW_TYPE,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(vm.find('.js-inline-diff-button').classes('active')).toBe(false);
|
expect(vm.find('.js-inline-diff-button').classes('selected')).toBe(false);
|
||||||
expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(true);
|
expect(vm.find('.js-parallel-diff-button').classes('selected')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls setInlineDiffViewType when clicking inline button', () => {
|
it('calls setInlineDiffViewType when clicking inline button', () => {
|
||||||
|
|
|
@ -9,6 +9,9 @@ import '~/behaviors/markdown/render_gfm';
|
||||||
import IssuableApp from '~/issue_show/components/app.vue';
|
import IssuableApp from '~/issue_show/components/app.vue';
|
||||||
import eventHub from '~/issue_show/event_hub';
|
import eventHub from '~/issue_show/event_hub';
|
||||||
import { initialRequest, secondRequest } from '../mock_data';
|
import { initialRequest, secondRequest } from '../mock_data';
|
||||||
|
import IncidentTabs from '~/issue_show/components/incident_tabs.vue';
|
||||||
|
import DescriptionComponent from '~/issue_show/components/description.vue';
|
||||||
|
import PinnedLinks from '~/issue_show/components/pinned_links.vue';
|
||||||
|
|
||||||
function formatText(text) {
|
function formatText(text) {
|
||||||
return text.trim().replace(/\s\s+/g, ' ');
|
return text.trim().replace(/\s\s+/g, ' ');
|
||||||
|
@ -22,6 +25,27 @@ const REALTIME_REQUEST_STACK = [initialRequest, secondRequest];
|
||||||
const zoomMeetingUrl = 'https://gitlab.zoom.us/j/95919234811';
|
const zoomMeetingUrl = 'https://gitlab.zoom.us/j/95919234811';
|
||||||
const publishedIncidentUrl = 'https://status.com/';
|
const publishedIncidentUrl = 'https://status.com/';
|
||||||
|
|
||||||
|
const defaultProps = {
|
||||||
|
canUpdate: true,
|
||||||
|
canDestroy: true,
|
||||||
|
endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes',
|
||||||
|
updateEndpoint: TEST_HOST,
|
||||||
|
issuableRef: '#1',
|
||||||
|
issuableStatus: 'opened',
|
||||||
|
initialTitleHtml: '',
|
||||||
|
initialTitleText: '',
|
||||||
|
initialDescriptionHtml: 'test',
|
||||||
|
initialDescriptionText: 'test',
|
||||||
|
lockVersion: 1,
|
||||||
|
markdownPreviewPath: '/',
|
||||||
|
markdownDocsPath: '/',
|
||||||
|
projectNamespace: '/',
|
||||||
|
projectPath: '/',
|
||||||
|
issuableTemplateNamesPath: '/issuable-templates-path',
|
||||||
|
zoomMeetingUrl,
|
||||||
|
publishedIncidentUrl,
|
||||||
|
};
|
||||||
|
|
||||||
describe('Issuable output', () => {
|
describe('Issuable output', () => {
|
||||||
useMockIntersectionObserver();
|
useMockIntersectionObserver();
|
||||||
|
|
||||||
|
@ -31,6 +55,12 @@ describe('Issuable output', () => {
|
||||||
|
|
||||||
const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]');
|
const findStickyHeader = () => wrapper.find('[data-testid="issue-sticky-header"]');
|
||||||
|
|
||||||
|
const mountComponent = (props = {}) => {
|
||||||
|
wrapper = mount(IssuableApp, {
|
||||||
|
propsData: { ...defaultProps, ...props },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setFixtures(`
|
setFixtures(`
|
||||||
<div>
|
<div>
|
||||||
|
@ -57,28 +87,7 @@ describe('Issuable output', () => {
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper = mount(IssuableApp, {
|
mountComponent();
|
||||||
propsData: {
|
|
||||||
canUpdate: true,
|
|
||||||
canDestroy: true,
|
|
||||||
endpoint: '/gitlab-org/gitlab-shell/-/issues/9/realtime_changes',
|
|
||||||
updateEndpoint: TEST_HOST,
|
|
||||||
issuableRef: '#1',
|
|
||||||
issuableStatus: 'opened',
|
|
||||||
initialTitleHtml: '',
|
|
||||||
initialTitleText: '',
|
|
||||||
initialDescriptionHtml: 'test',
|
|
||||||
initialDescriptionText: 'test',
|
|
||||||
lockVersion: 1,
|
|
||||||
markdownPreviewPath: '/',
|
|
||||||
markdownDocsPath: '/',
|
|
||||||
projectNamespace: '/',
|
|
||||||
projectPath: '/',
|
|
||||||
issuableTemplateNamesPath: '/issuable-templates-path',
|
|
||||||
zoomMeetingUrl,
|
|
||||||
publishedIncidentUrl,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -562,4 +571,46 @@ describe('Issuable output', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Composable description component', () => {
|
||||||
|
const findIncidentTabs = () => wrapper.find(IncidentTabs);
|
||||||
|
const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
|
||||||
|
const findPinnedLinks = () => wrapper.find(PinnedLinks);
|
||||||
|
const borderClass = 'gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid gl-mb-6';
|
||||||
|
|
||||||
|
describe('when using description component', () => {
|
||||||
|
it('renders the description component', () => {
|
||||||
|
expect(findDescriptionComponent().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render incident tabs', () => {
|
||||||
|
expect(findIncidentTabs().exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a border below the header', () => {
|
||||||
|
expect(findPinnedLinks().attributes('class')).toContain(borderClass);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when using incident tabs description wrapper', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent({
|
||||||
|
descriptionComponent: IncidentTabs,
|
||||||
|
showTitleBorder: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the description component', () => {
|
||||||
|
expect(findDescriptionComponent().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders incident tabs', () => {
|
||||||
|
expect(findIncidentTabs().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not add a border below the header', () => {
|
||||||
|
expect(findPinnedLinks().attributes('class')).not.toContain(borderClass);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,20 +5,13 @@ import mountComponent from 'helpers/vue_mount_component_helper';
|
||||||
import { TEST_HOST } from 'helpers/test_constants';
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
import Description from '~/issue_show/components/description.vue';
|
import Description from '~/issue_show/components/description.vue';
|
||||||
import TaskList from '~/task_list';
|
import TaskList from '~/task_list';
|
||||||
|
import { descriptionProps as props } from '../mock_data';
|
||||||
|
|
||||||
jest.mock('~/task_list');
|
jest.mock('~/task_list');
|
||||||
|
|
||||||
describe('Description component', () => {
|
describe('Description component', () => {
|
||||||
let vm;
|
let vm;
|
||||||
let DescriptionComponent;
|
let DescriptionComponent;
|
||||||
const props = {
|
|
||||||
canUpdate: true,
|
|
||||||
descriptionHtml: 'test',
|
|
||||||
descriptionText: 'test',
|
|
||||||
updatedAt: new Date().toString(),
|
|
||||||
taskStatus: '',
|
|
||||||
updateUrl: TEST_HOST,
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
DescriptionComponent = Vue.extend(Description);
|
DescriptionComponent = Vue.extend(Description);
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { shallowMount } from '@vue/test-utils';
|
||||||
|
import { GlTab } from '@gitlab/ui';
|
||||||
|
import IncidentTabs from '~/issue_show/components/incident_tabs.vue';
|
||||||
|
import { descriptionProps } from '../mock_data';
|
||||||
|
import DescriptionComponent from '~/issue_show/components/description.vue';
|
||||||
|
|
||||||
|
describe('Incident Tabs component', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
const mountComponent = () => {
|
||||||
|
wrapper = shallowMount(IncidentTabs, {
|
||||||
|
propsData: {
|
||||||
|
...descriptionProps,
|
||||||
|
},
|
||||||
|
stubs: {
|
||||||
|
DescriptionComponent: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mountComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
const findTabs = () => wrapper.findAll(GlTab);
|
||||||
|
const findSummaryTab = () => findTabs().at(0);
|
||||||
|
const findDescriptionComponent = () => wrapper.find(DescriptionComponent);
|
||||||
|
|
||||||
|
describe('default state', () => {
|
||||||
|
it('renders the summary tab', async () => {
|
||||||
|
expect(findTabs()).toHaveLength(1);
|
||||||
|
expect(findSummaryTab().exists()).toBe(true);
|
||||||
|
expect(findSummaryTab().attributes('title')).toBe('Summary');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders the description component', () => {
|
||||||
|
expect(findDescriptionComponent().exists()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes all props to the description component', () => {
|
||||||
|
expect(findDescriptionComponent().props()).toMatchObject(descriptionProps);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,19 +0,0 @@
|
||||||
import initIssueableApp from '~/issue_show';
|
|
||||||
|
|
||||||
describe('Issue show index', () => {
|
|
||||||
describe('initIssueableApp', () => {
|
|
||||||
it('should initialize app with no potential XSS attack', () => {
|
|
||||||
const d = document.createElement('div');
|
|
||||||
d.id = 'js-issuable-app-initial-data';
|
|
||||||
d.innerHTML = JSON.stringify({
|
|
||||||
initialDescriptionHtml: '<img src=x onerror=alert(1)>',
|
|
||||||
});
|
|
||||||
document.body.appendChild(d);
|
|
||||||
|
|
||||||
const alertSpy = jest.spyOn(window, 'alert');
|
|
||||||
initIssueableApp();
|
|
||||||
|
|
||||||
expect(alertSpy).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import initIssuableApp from '~/issue_show/issue';
|
||||||
|
import { parseIssuableData } from '~/issue_show/utils/parse_data';
|
||||||
|
|
||||||
|
describe('Issue show index', () => {
|
||||||
|
describe('initIssueableApp', () => {
|
||||||
|
// Warning: this test is currently faulty.
|
||||||
|
// More details at https://gitlab.com/gitlab-org/gitlab/-/issues/241717
|
||||||
|
// eslint-disable-next-line jest/no-disabled-tests
|
||||||
|
it.skip('should initialize app with no potential XSS attack', () => {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.id = 'js-issuable-app-initial-data';
|
||||||
|
|
||||||
|
d.innerHTML = JSON.stringify({
|
||||||
|
initialDescriptionHtml: '<img src=x onerror=alert(1)>',
|
||||||
|
});
|
||||||
|
|
||||||
|
document.body.appendChild(d);
|
||||||
|
|
||||||
|
const alertSpy = jest.spyOn(window, 'alert');
|
||||||
|
const issuableData = parseIssuableData();
|
||||||
|
initIssuableApp(issuableData);
|
||||||
|
|
||||||
|
expect(alertSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { TEST_HOST } from 'helpers/test_constants';
|
||||||
|
|
||||||
export const initialRequest = {
|
export const initialRequest = {
|
||||||
title: '<p>this is a title</p>',
|
title: '<p>this is a title</p>',
|
||||||
title_text: 'this is a title',
|
title_text: 'this is a title',
|
||||||
|
@ -21,3 +23,11 @@ export const secondRequest = {
|
||||||
updated_by_path: '/other_user',
|
updated_by_path: '/other_user',
|
||||||
lock_version: 2,
|
lock_version: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const descriptionProps = {
|
||||||
|
canUpdate: true,
|
||||||
|
descriptionHtml: 'test',
|
||||||
|
descriptionText: 'test',
|
||||||
|
taskStatus: '',
|
||||||
|
updateUrl: TEST_HOST,
|
||||||
|
};
|
||||||
|
|
|
@ -206,6 +206,33 @@ RSpec.describe Resolvers::MergeRequestsResolver do
|
||||||
expect(result.compact).to contain_exactly(merge_request_4)
|
expect(result.compact).to contain_exactly(merge_request_4)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'sorting' do
|
||||||
|
context 'when sorting by created' do
|
||||||
|
it 'sorts merge requests ascending' do
|
||||||
|
expect(resolve_mr(project, sort: 'created_asc')).to eq [merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6, merge_request_with_milestone]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts merge requests descending' do
|
||||||
|
expect(resolve_mr(project, sort: 'created_desc')).to eq [merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_3, merge_request_2, merge_request_1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sorting by merged at' do
|
||||||
|
before do
|
||||||
|
merge_request_1.metrics.update!(merged_at: 10.days.ago)
|
||||||
|
merge_request_3.metrics.update!(merged_at: 5.days.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts merge requests ascending' do
|
||||||
|
expect(resolve_mr(project, sort: :merged_at_asc)).to eq [merge_request_1, merge_request_3, merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_2]
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts merge requests descending' do
|
||||||
|
expect(resolve_mr(project, sort: :merged_at_desc)).to eq [merge_request_3, merge_request_1, merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_2]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def resolve_mr_single(project, iid)
|
def resolve_mr_single(project, iid)
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe GitlabSchema.types['MergeRequestSort'] do
|
||||||
|
specify { expect(described_class.graphql_name).to eq('MergeRequestSort') }
|
||||||
|
|
||||||
|
it_behaves_like 'common sort values'
|
||||||
|
|
||||||
|
it 'exposes all the existing issue sort values' do
|
||||||
|
expect(described_class.values.keys).to include(
|
||||||
|
*%w[MERGED_AT_ASC MERGED_AT_DESC]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -75,7 +75,8 @@ RSpec.describe GitlabSchema.types['Project'] do
|
||||||
:merged_before,
|
:merged_before,
|
||||||
:author_username,
|
:author_username,
|
||||||
:assignee_username,
|
:assignee_username,
|
||||||
:milestone_title
|
:milestone_title,
|
||||||
|
:sort
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe ContainerRegistryHelper do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
describe '#limit_delete_tags_service?' do
|
||||||
|
subject { helper.limit_delete_tags_service? }
|
||||||
|
|
||||||
|
where(:feature_flag_enabled, :client_support, :expected_result) do
|
||||||
|
true | true | true
|
||||||
|
true | false | false
|
||||||
|
false | true | false
|
||||||
|
false | false | false
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(container_registry_expiration_policies_throttling: feature_flag_enabled)
|
||||||
|
allow(ContainerRegistry::Client).to receive(:supports_tag_delete?).and_return(client_support)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to eq(expected_result) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -197,7 +197,8 @@ RSpec.describe IssuablesHelper do
|
||||||
initialTitleText: issue.title,
|
initialTitleText: issue.title,
|
||||||
initialDescriptionHtml: '<p dir="auto">issue text</p>',
|
initialDescriptionHtml: '<p dir="auto">issue text</p>',
|
||||||
initialDescriptionText: 'issue text',
|
initialDescriptionText: 'issue text',
|
||||||
initialTaskStatus: '0 of 0 tasks completed'
|
initialTaskStatus: '0 of 0 tasks completed',
|
||||||
|
issueType: 'issue'
|
||||||
}
|
}
|
||||||
expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data))
|
expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data))
|
||||||
end
|
end
|
||||||
|
|
|
@ -289,4 +289,57 @@ RSpec.describe ContainerRegistry::Client do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.supports_tag_delete?' do
|
||||||
|
let(:registry_enabled) { true }
|
||||||
|
let(:registry_api_url) { 'http://sandbox.local' }
|
||||||
|
let(:registry_tags_support_enabled) { true }
|
||||||
|
let(:is_on_dot_com) { false }
|
||||||
|
|
||||||
|
subject { described_class.supports_tag_delete? }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(::Gitlab).to receive(:com?).and_return(is_on_dot_com)
|
||||||
|
stub_container_registry_config(enabled: registry_enabled, api_url: registry_api_url, key: 'spec/fixtures/x509_certificate_pk.key')
|
||||||
|
stub_registry_tags_support(registry_tags_support_enabled)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the registry enabled' do
|
||||||
|
it { is_expected.to be true }
|
||||||
|
|
||||||
|
context 'without an api url' do
|
||||||
|
let(:registry_api_url) { '' }
|
||||||
|
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'on .com' do
|
||||||
|
let(:is_on_dot_com) { true }
|
||||||
|
|
||||||
|
it { is_expected.to be true }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when registry server does not support tag deletion' do
|
||||||
|
let(:registry_tags_support_enabled) { false }
|
||||||
|
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the registry disabled' do
|
||||||
|
let(:registry_enabled) { false }
|
||||||
|
|
||||||
|
it { is_expected.to be false }
|
||||||
|
end
|
||||||
|
|
||||||
|
def stub_registry_tags_support(supported = true)
|
||||||
|
status_code = supported ? 200 : 404
|
||||||
|
stub_request(:options, "#{registry_api_url}/v2/name/tags/reference/tag")
|
||||||
|
.to_return(
|
||||||
|
status: status_code,
|
||||||
|
body: '',
|
||||||
|
headers: { 'Allow' => 'DELETE' }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -71,6 +71,8 @@ RSpec.describe ApplicationSetting do
|
||||||
it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) }
|
it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) }
|
||||||
it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
|
it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) }
|
||||||
|
|
||||||
|
it { is_expected.to validate_numericality_of(:container_registry_delete_tags_service_timeout).only_integer.is_greater_than_or_equal_to(0) }
|
||||||
|
|
||||||
it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) }
|
it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) }
|
||||||
it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) }
|
it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) }
|
||||||
it { is_expected.to validate_presence_of(:max_artifacts_size) }
|
it { is_expected.to validate_presence_of(:max_artifacts_size) }
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Atlassian::Identity do
|
||||||
|
describe 'associations' do
|
||||||
|
it { is_expected.to belong_to(:user) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'validations' do
|
||||||
|
subject { create(:atlassian_identity) }
|
||||||
|
|
||||||
|
it { is_expected.to validate_presence_of(:extern_uid) }
|
||||||
|
it { is_expected.to validate_uniqueness_of(:extern_uid) }
|
||||||
|
it { is_expected.to validate_presence_of(:user) }
|
||||||
|
it { is_expected.to validate_uniqueness_of(:user) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'encrypted tokens' do
|
||||||
|
let(:token) { SecureRandom.alphanumeric(1254) }
|
||||||
|
let(:refresh_token) { SecureRandom.alphanumeric(45) }
|
||||||
|
let(:identity) { create(:atlassian_identity, token: token, refresh_token: refresh_token) }
|
||||||
|
|
||||||
|
it 'saves the encrypted token, refresh token and corresponding ivs' do
|
||||||
|
expect(identity.encrypted_token).not_to be_nil
|
||||||
|
expect(identity.encrypted_token_iv).not_to be_nil
|
||||||
|
expect(identity.encrypted_refresh_token).not_to be_nil
|
||||||
|
expect(identity.encrypted_refresh_token_iv).not_to be_nil
|
||||||
|
|
||||||
|
expect(identity.token).to eq(token)
|
||||||
|
expect(identity.refresh_token).to eq(refresh_token)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -61,6 +61,24 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.order_merged_at_asc' do
|
||||||
|
let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
|
||||||
|
let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }
|
||||||
|
|
||||||
|
it 'returns MRs ordered by merged_at ascending' do
|
||||||
|
expect(described_class.order_merged_at_asc).to eq([older_mr, newer_mr])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.order_merged_at_desc' do
|
||||||
|
let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
|
||||||
|
let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }
|
||||||
|
|
||||||
|
it 'returns MRs ordered by merged_at descending' do
|
||||||
|
expect(described_class.order_merged_at_desc).to eq([newer_mr, older_mr])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#squash_in_progress?' do
|
describe '#squash_in_progress?' do
|
||||||
let(:repo_path) do
|
let(:repo_path) do
|
||||||
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
|
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
|
||||||
|
@ -431,6 +449,23 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.sort_by_attribute' do
|
||||||
|
context 'merged_at' do
|
||||||
|
let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) }
|
||||||
|
let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) }
|
||||||
|
|
||||||
|
it 'sorts asc' do
|
||||||
|
merge_requests = described_class.sort_by_attribute(:merged_at_asc)
|
||||||
|
expect(merge_requests).to eq([older_mr, newer_mr])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts desc' do
|
||||||
|
merge_requests = described_class.sort_by_attribute(:merged_at_desc)
|
||||||
|
expect(merge_requests).to eq([newer_mr, older_mr])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#target_branch_sha' do
|
describe '#target_branch_sha' do
|
||||||
let(:project) { create(:project, :repository) }
|
let(:project) { create(:project, :repository) }
|
||||||
|
|
||||||
|
|
|
@ -171,6 +171,16 @@ RSpec.describe Service do
|
||||||
it { is_expected.to be_falsey }
|
it { is_expected.to be_falsey }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when group-level service' do
|
||||||
|
Service.available_services_types.each do |service_type|
|
||||||
|
let(:service) do
|
||||||
|
service_type.constantize.new(group_id: group.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to be_falsey }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#test' do
|
describe '#test' do
|
||||||
|
|
|
@ -787,4 +787,26 @@ RSpec.describe Snippet do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#multiple_files?' do
|
||||||
|
subject { snippet.multiple_files? }
|
||||||
|
|
||||||
|
context 'when snippet has multiple files' do
|
||||||
|
let(:snippet) { create(:snippet, :repository) }
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when snippet does not have multiple files' do
|
||||||
|
let(:snippet) { create(:snippet, :empty_repo) }
|
||||||
|
|
||||||
|
it { is_expected.to be_falsey }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the snippet does not have a repository' do
|
||||||
|
let(:snippet) { build(:snippet) }
|
||||||
|
|
||||||
|
it { is_expected.to be_falsey }
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -68,6 +68,7 @@ RSpec.describe User do
|
||||||
it { is_expected.to have_one(:namespace) }
|
it { is_expected.to have_one(:namespace) }
|
||||||
it { is_expected.to have_one(:status) }
|
it { is_expected.to have_one(:status) }
|
||||||
it { is_expected.to have_one(:user_detail) }
|
it { is_expected.to have_one(:user_detail) }
|
||||||
|
it { is_expected.to have_one(:atlassian_identity) }
|
||||||
it { is_expected.to have_one(:user_highest_role) }
|
it { is_expected.to have_one(:user_highest_role) }
|
||||||
it { is_expected.to have_many(:snippets).dependent(:destroy) }
|
it { is_expected.to have_many(:snippets).dependent(:destroy) }
|
||||||
it { is_expected.to have_many(:members) }
|
it { is_expected.to have_many(:members) }
|
||||||
|
|
|
@ -12,11 +12,11 @@ RSpec.describe "uploading designs" do
|
||||||
let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] }
|
let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] }
|
||||||
let(:variables) { {} }
|
let(:variables) { {} }
|
||||||
|
|
||||||
let(:mutation) do
|
def mutation
|
||||||
input = {
|
input = {
|
||||||
project_path: project.full_path,
|
project_path: project.full_path,
|
||||||
iid: issue.iid,
|
iid: issue.iid,
|
||||||
files: files
|
files: files.dup
|
||||||
}.merge(variables)
|
}.merge(variables)
|
||||||
graphql_mutation(:design_management_upload, input)
|
graphql_mutation(:design_management_upload, input)
|
||||||
end
|
end
|
||||||
|
@ -30,31 +30,15 @@ RSpec.describe "uploading designs" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns an error if the user is not allowed to upload designs" do
|
it "returns an error if the user is not allowed to upload designs" do
|
||||||
post_graphql_mutation(mutation, current_user: create(:user))
|
post_graphql_mutation_with_uploads(mutation, current_user: create(:user))
|
||||||
|
|
||||||
expect(graphql_errors).to be_present
|
expect(graphql_errors).to be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
it "succeeds (backward compatibility)" do
|
it "succeeds, and responds with the created designs" do
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||||
|
|
||||||
expect(graphql_errors).not_to be_present
|
expect(graphql_errors).not_to be_present
|
||||||
end
|
|
||||||
|
|
||||||
it 'succeeds' do
|
|
||||||
file_path_in_params = ['designManagementUploadInput', 'files', 0]
|
|
||||||
params = mutation_to_apollo_uploads_param(mutation, files: [file_path_in_params])
|
|
||||||
|
|
||||||
workhorse_post_with_file(api('/', current_user, version: 'graphql'),
|
|
||||||
params: params,
|
|
||||||
file_key: '1'
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(graphql_errors).not_to be_present
|
|
||||||
end
|
|
||||||
|
|
||||||
it "responds with the created designs" do
|
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
|
||||||
|
|
||||||
expect(mutation_response).to include(
|
expect(mutation_response).to include(
|
||||||
"designs" => a_collection_containing_exactly(
|
"designs" => a_collection_containing_exactly(
|
||||||
|
@ -65,7 +49,7 @@ RSpec.describe "uploading designs" do
|
||||||
|
|
||||||
it "can respond with skipped designs" do
|
it "can respond with skipped designs" do
|
||||||
2.times do
|
2.times do
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||||
files.each(&:rewind)
|
files.each(&:rewind)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -80,7 +64,7 @@ RSpec.describe "uploading designs" do
|
||||||
let(:variables) { { iid: "123" } }
|
let(:variables) { { iid: "123" } }
|
||||||
|
|
||||||
it "returns an error" do
|
it "returns an error" do
|
||||||
post_graphql_mutation(mutation, current_user: create(:user))
|
post_graphql_mutation_with_uploads(mutation, current_user: create(:user))
|
||||||
|
|
||||||
expect(graphql_errors).not_to be_empty
|
expect(graphql_errors).not_to be_empty
|
||||||
end
|
end
|
||||||
|
@ -92,7 +76,7 @@ RSpec.describe "uploading designs" do
|
||||||
expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" })
|
expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" })
|
||||||
end
|
end
|
||||||
|
|
||||||
post_graphql_mutation(mutation, current_user: current_user)
|
post_graphql_mutation_with_uploads(mutation, current_user: current_user)
|
||||||
expect(mutation_response["errors"].first).to eq("Something went wrong")
|
expect(mutation_response["errors"].first).to eq("Something went wrong")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -210,4 +210,48 @@ RSpec.describe 'getting merge request listings nested in a project' do
|
||||||
include_examples 'N+1 query check'
|
include_examples 'N+1 query check'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
describe 'sorting and pagination' do
|
||||||
|
let(:data_path) { [:project, :mergeRequests] }
|
||||||
|
|
||||||
|
def pagination_query(params, page_info)
|
||||||
|
graphql_query_for(
|
||||||
|
:project,
|
||||||
|
{ full_path: project.full_path },
|
||||||
|
<<~QUERY
|
||||||
|
mergeRequests(#{params}) {
|
||||||
|
#{page_info} edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QUERY
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pagination_results_data(data)
|
||||||
|
data.map { |project| project.dig('node', 'id') }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when sorting by merged_at DESC' do
|
||||||
|
it_behaves_like 'sorted paginated query' do
|
||||||
|
let(:sort_param) { 'MERGED_AT_DESC' }
|
||||||
|
let(:first_param) { 2 }
|
||||||
|
|
||||||
|
let(:expected_results) do
|
||||||
|
[
|
||||||
|
merge_request_b,
|
||||||
|
merge_request_c,
|
||||||
|
merge_request_d,
|
||||||
|
merge_request_a
|
||||||
|
].map(&:to_gid).map(&:to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
merge_request_c.metrics.update!(merged_at: 5.days.ago)
|
||||||
|
merge_request_b.metrics.update!(merged_at: 1.day.ago)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -391,20 +391,97 @@ RSpec.describe API::Snippets do
|
||||||
create(:personal_snippet, :repository, author: user, visibility_level: visibility_level)
|
create(:personal_snippet, :repository, author: user, visibility_level: visibility_level)
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'snippet updates' do
|
let(:create_action) { { action: 'create', file_path: 'foo.txt', content: 'bar' } }
|
||||||
it 'updates a snippet' do
|
let(:update_action) { { action: 'update', file_path: 'CHANGELOG', content: 'bar' } }
|
||||||
new_content = 'New content'
|
let(:move_action) { { action: 'move', file_path: '.old-gitattributes', previous_path: '.gitattributes' } }
|
||||||
|
let(:delete_action) { { action: 'delete', file_path: 'CONTRIBUTING.md' } }
|
||||||
|
let(:bad_file_path) { { action: 'create', file_path: '../../etc/passwd', content: 'bar' } }
|
||||||
|
let(:bad_previous_path) { { action: 'create', previous_path: '../../etc/passwd', file_path: 'CHANGELOG', content: 'bar' } }
|
||||||
|
let(:invalid_move) { { action: 'move', file_path: 'missing_previous_path.txt' } }
|
||||||
|
|
||||||
|
context 'with snippet file changes' do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
where(:is_multi_file, :file_name, :content, :files, :status) do
|
||||||
|
true | nil | nil | [create_action] | :success
|
||||||
|
true | nil | nil | [update_action] | :success
|
||||||
|
true | nil | nil | [move_action] | :success
|
||||||
|
true | nil | nil | [delete_action] | :success
|
||||||
|
true | nil | nil | [create_action, update_action] | :success
|
||||||
|
true | 'foo.txt' | 'bar' | [create_action] | :bad_request
|
||||||
|
true | 'foo.txt' | 'bar' | nil | :bad_request
|
||||||
|
true | nil | nil | nil | :bad_request
|
||||||
|
true | 'foo.txt' | nil | [create_action] | :bad_request
|
||||||
|
true | nil | 'bar' | [create_action] | :bad_request
|
||||||
|
true | '' | nil | [create_action] | :bad_request
|
||||||
|
true | nil | '' | [create_action] | :bad_request
|
||||||
|
true | nil | nil | [bad_file_path] | :bad_request
|
||||||
|
true | nil | nil | [bad_previous_path] | :bad_request
|
||||||
|
true | nil | nil | [invalid_move] | :forbidden
|
||||||
|
|
||||||
|
false | 'foo.txt' | 'bar' | nil | :success
|
||||||
|
false | 'foo.txt' | nil | nil | :success
|
||||||
|
false | nil | 'bar' | nil | :success
|
||||||
|
false | 'foo.txt' | 'bar' | [create_action] | :bad_request
|
||||||
|
false | nil | nil | nil | :bad_request
|
||||||
|
false | nil | '' | nil | :bad_request
|
||||||
|
false | nil | nil | [bad_file_path] | :bad_request
|
||||||
|
false | nil | nil | [bad_previous_path] | :bad_request
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(is_multi_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the correct response' do
|
||||||
|
update_params = {}.tap do |params|
|
||||||
|
params[:files] = files if files
|
||||||
|
params[:file_name] = file_name if file_name
|
||||||
|
params[:content] = content if content
|
||||||
|
end
|
||||||
|
|
||||||
|
update_snippet(params: update_params)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when save fails due to a repository commit error' do
|
||||||
|
before do
|
||||||
|
allow_next_instance_of(Repository) do |instance|
|
||||||
|
allow(instance).to receive(:multi_action).and_raise(Gitlab::Git::CommitError)
|
||||||
|
end
|
||||||
|
|
||||||
|
update_snippet(params: { files: [create_action] })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a bad request response' do
|
||||||
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'snippet non-file updates' do
|
||||||
|
it 'updates a snippet non-file attributes' do
|
||||||
new_description = 'New description'
|
new_description = 'New description'
|
||||||
|
new_title = 'New title'
|
||||||
|
new_visibility = 'internal'
|
||||||
|
|
||||||
update_snippet(params: { content: new_content, description: new_description, visibility: 'internal' })
|
update_snippet(params: { title: new_title, description: new_description, visibility: new_visibility })
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
|
||||||
snippet.reload
|
snippet.reload
|
||||||
expect(snippet.content).to eq(new_content)
|
|
||||||
|
aggregate_failures do
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(snippet.description).to eq(new_description)
|
expect(snippet.description).to eq(new_description)
|
||||||
expect(snippet.visibility).to eq('internal')
|
expect(snippet.visibility).to eq(new_visibility)
|
||||||
|
expect(snippet.title).to eq(new_title)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'snippet non-file updates'
|
||||||
|
|
||||||
context 'with restricted visibility settings' do
|
context 'with restricted visibility settings' do
|
||||||
before do
|
before do
|
||||||
|
@ -413,11 +490,9 @@ RSpec.describe API::Snippets do
|
||||||
Gitlab::VisibilityLevel::PRIVATE])
|
Gitlab::VisibilityLevel::PRIVATE])
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'snippet updates'
|
it_behaves_like 'snippet non-file updates'
|
||||||
end
|
end
|
||||||
|
|
||||||
it_behaves_like 'snippet updates'
|
|
||||||
|
|
||||||
it 'returns 404 for invalid snippet id' do
|
it 'returns 404 for invalid snippet id' do
|
||||||
update_snippet(snippet_id: non_existing_record_id, params: { title: 'Foo' })
|
update_snippet(snippet_id: non_existing_record_id, params: { title: 'Foo' })
|
||||||
|
|
||||||
|
@ -438,13 +513,6 @@ RSpec.describe API::Snippets do
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
expect(response).to have_gitlab_http_status(:bad_request)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns 400 if content is blank' do
|
|
||||||
update_snippet(params: { content: '' })
|
|
||||||
|
|
||||||
expect(response).to have_gitlab_http_status(:bad_request)
|
|
||||||
expect(json_response['error']).to eq 'content is empty'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns 400 if title is blank' do
|
it 'returns 400 if title is blank' do
|
||||||
update_snippet(params: { title: '' })
|
update_snippet(params: { title: '' })
|
||||||
|
|
||||||
|
|
|
@ -66,12 +66,13 @@ RSpec.describe Ci::ParseDotenvArtifactService do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when multiple key/value pairs exist in one line' do
|
context 'when multiple key/value pairs exist in one line' do
|
||||||
let(:blob) { 'KEY1=VAR1KEY2=VAR1' }
|
let(:blob) { 'KEY=VARCONTAINING=EQLS' }
|
||||||
|
|
||||||
it 'returns error' do
|
it 'parses the dotenv data' do
|
||||||
expect(subject[:status]).to eq(:error)
|
subject
|
||||||
expect(subject[:message]).to eq("Validation failed: Key can contain only letters, digits and '_'.")
|
|
||||||
expect(subject[:http_status]).to eq(:bad_request)
|
expect(build.job_variables.as_json).to contain_exactly(
|
||||||
|
hash_including('key' => 'KEY', 'value' => 'VARCONTAINING=EQLS'))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -90,6 +90,10 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
|
||||||
|
|
||||||
subject { service.execute(repository) }
|
subject { service.execute(repository) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(container_registry_expiration_policies_throttling: false)
|
||||||
|
end
|
||||||
|
|
||||||
context 'without permissions' do
|
context 'without permissions' do
|
||||||
it { is_expected.to include(status: :error) }
|
it { is_expected.to include(status: :error) }
|
||||||
end
|
end
|
||||||
|
@ -119,6 +123,18 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do
|
||||||
|
|
||||||
it_behaves_like 'logging a success response'
|
it_behaves_like 'logging a success response'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with a timeout error' do
|
||||||
|
before do
|
||||||
|
expect_next_instance_of(::Projects::ContainerRepository::Gitlab::DeleteTagsService) do |delete_service|
|
||||||
|
expect(delete_service).to receive(:delete_tags).and_raise(::Projects::ContainerRepository::Gitlab::DeleteTagsService::TimeoutError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to include(status: :error, message: 'timeout while deleting tags') }
|
||||||
|
|
||||||
|
it_behaves_like 'logging an error response', message: 'timeout while deleting tags'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'and the feature is disabled' do
|
context 'and the feature is disabled' do
|
||||||
|
|
|
@ -12,13 +12,21 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService do
|
||||||
|
|
||||||
subject { service.execute }
|
subject { service.execute }
|
||||||
|
|
||||||
context 'with tags to delete' do
|
before do
|
||||||
|
stub_feature_flags(container_registry_expiration_policies_throttling: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples 'deleting tags' do
|
||||||
it 'deletes the tags by name' do
|
it 'deletes the tags by name' do
|
||||||
stub_delete_reference_requests(tags)
|
stub_delete_reference_requests(tags)
|
||||||
expect_delete_tag_by_names(tags)
|
expect_delete_tag_by_names(tags)
|
||||||
|
|
||||||
is_expected.to eq(status: :success, deleted: tags)
|
is_expected.to eq(status: :success, deleted: tags)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with tags to delete' do
|
||||||
|
it_behaves_like 'deleting tags'
|
||||||
|
|
||||||
it 'succeeds when tag delete returns 404' do
|
it 'succeeds when tag delete returns 404' do
|
||||||
stub_delete_reference_requests('A' => 200, 'Ba' => 404)
|
stub_delete_reference_requests('A' => 200, 'Ba' => 404)
|
||||||
|
@ -41,6 +49,47 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService do
|
||||||
it { is_expected.to eq(status: :error, message: 'could not delete tags') }
|
it { is_expected.to eq(status: :error, message: 'could not delete tags') }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with throttling enabled' do
|
||||||
|
let(:timeout) { 10 }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_feature_flags(container_registry_expiration_policies_throttling: true)
|
||||||
|
stub_application_setting(container_registry_delete_tags_service_timeout: timeout)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'deleting tags'
|
||||||
|
|
||||||
|
context 'with timeout' do
|
||||||
|
context 'set to a valid value' do
|
||||||
|
before do
|
||||||
|
allow(Time.zone).to receive(:now).and_return(10, 15, 25) # third call to Time.zone.now will be triggering the timeout
|
||||||
|
stub_delete_reference_requests('A' => 200)
|
||||||
|
end
|
||||||
|
|
||||||
|
it { is_expected.to include(status: :error, message: 'timeout while deleting tags') }
|
||||||
|
|
||||||
|
it 'tracks the exception' do
|
||||||
|
expect(::Gitlab::ErrorTracking)
|
||||||
|
.to receive(:track_exception).with(::Projects::ContainerRepository::Gitlab::DeleteTagsService::TimeoutError, tags_count: tags.size, container_repository_id: repository.id)
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'set to 0' do
|
||||||
|
let(:timeout) { 0 }
|
||||||
|
|
||||||
|
it_behaves_like 'deleting tags'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'set to nil' do
|
||||||
|
let(:timeout) { nil }
|
||||||
|
|
||||||
|
it_behaves_like 'deleting tags'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with empty tags' do
|
context 'with empty tags' do
|
||||||
|
|
|
@ -241,6 +241,39 @@ module GraphqlHelpers
|
||||||
post_graphql(mutation.query, current_user: current_user, variables: mutation.variables)
|
post_graphql(mutation.query, current_user: current_user, variables: mutation.variables)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def post_graphql_mutation_with_uploads(mutation, current_user: nil)
|
||||||
|
file_paths = file_paths_in_mutation(mutation)
|
||||||
|
params = mutation_to_apollo_uploads_param(mutation, files: file_paths)
|
||||||
|
|
||||||
|
workhorse_post_with_file(api('/', current_user, version: 'graphql'),
|
||||||
|
params: params,
|
||||||
|
file_key: '1'
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def file_paths_in_mutation(mutation)
|
||||||
|
paths = []
|
||||||
|
find_uploads(paths, [], mutation.variables)
|
||||||
|
|
||||||
|
paths
|
||||||
|
end
|
||||||
|
|
||||||
|
# Depth first search for UploadedFile values
|
||||||
|
def find_uploads(paths, path, value)
|
||||||
|
case value
|
||||||
|
when Rack::Test::UploadedFile
|
||||||
|
paths << path
|
||||||
|
when Hash
|
||||||
|
value.each do |k, v|
|
||||||
|
find_uploads(paths, path + [k], v)
|
||||||
|
end
|
||||||
|
when Array
|
||||||
|
value.each_with_index do |v, i|
|
||||||
|
find_uploads(paths, path + [i], v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# this implements GraphQL multipart request v2
|
# this implements GraphQL multipart request v2
|
||||||
# https://github.com/jaydenseric/graphql-multipart-request-spec/tree/v2.0.0-alpha.2
|
# https://github.com/jaydenseric/graphql-multipart-request-spec/tree/v2.0.0-alpha.2
|
||||||
# this is simplified and do not support file deduplication
|
# this is simplified and do not support file deduplication
|
||||||
|
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
RSpec.shared_examples 'update with repository actions' do
|
RSpec.shared_examples 'update with repository actions' do
|
||||||
context 'when the repository exists' do
|
context 'when the repository exists' do
|
||||||
|
before do
|
||||||
|
allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(false)
|
||||||
|
end
|
||||||
|
|
||||||
it 'commits the changes to the repository' do
|
it 'commits the changes to the repository' do
|
||||||
existing_blob = snippet.blobs.first
|
existing_blob = snippet.blobs.first
|
||||||
new_file_name = existing_blob.path + '_new'
|
new_file_name = existing_blob.path + '_new'
|
||||||
|
|
Loading…
Reference in New Issue