Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
526f1e9859
commit
8280fa786e
|
@ -1 +1 @@
|
|||
ac72695adc90343b7255869818376f505bde8315
|
||||
6944c95243e2b6ed8f783ae903cb9b03ad50e0f9
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
<script>
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
|
||||
import getPipelineQuery from './graphql/queries/pipeline.query.graphql';
|
||||
import BridgeEmptyState from './components/empty_state.vue';
|
||||
import BridgeSidebar from './components/sidebar.vue';
|
||||
import { SIDEBAR_COLLAPSE_BREAKPOINTS } from './components/constants';
|
||||
|
||||
export default {
|
||||
name: 'BridgePageApp',
|
||||
components: {
|
||||
BridgeEmptyState,
|
||||
BridgeSidebar,
|
||||
CiHeader,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
inject: ['buildId', 'projectFullPath', 'pipelineIid'],
|
||||
apollo: {
|
||||
pipeline: {
|
||||
query: getPipelineQuery,
|
||||
variables() {
|
||||
return {
|
||||
fullPath: this.projectFullPath,
|
||||
iid: this.pipelineIid,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
if (!data?.project?.pipeline) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { pipeline } = data.project;
|
||||
const stages = pipeline?.stages.edges.map((edge) => edge.node) || [];
|
||||
const jobs = stages.map((stage) => stage.jobs.nodes).flat();
|
||||
|
||||
return {
|
||||
...pipeline,
|
||||
commit: {
|
||||
...pipeline.commit,
|
||||
commit_path: pipeline.commit.webPath,
|
||||
short_id: pipeline.commit.shortId,
|
||||
},
|
||||
id: getIdFromGraphQLId(pipeline.id),
|
||||
jobs,
|
||||
stages,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSidebarExpanded: true,
|
||||
pipeline: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
bridgeJob() {
|
||||
return (
|
||||
this.pipeline.jobs?.filter(
|
||||
(job) => getIdFromGraphQLId(job.id) === Number(this.buildId),
|
||||
)[0] || {}
|
||||
);
|
||||
},
|
||||
bridgeName() {
|
||||
return sprintf(__('Job %{jobName}'), { jobName: this.bridgeJob.name });
|
||||
},
|
||||
isPipelineLoading() {
|
||||
return this.$apollo.queries.pipeline.loading;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('resize', this.onResize);
|
||||
},
|
||||
mounted() {
|
||||
this.onResize();
|
||||
},
|
||||
methods: {
|
||||
toggleSidebar() {
|
||||
this.isSidebarExpanded = !this.isSidebarExpanded;
|
||||
},
|
||||
onResize() {
|
||||
const breakpoint = bp.getBreakpointSize();
|
||||
if (SIDEBAR_COLLAPSE_BREAKPOINTS.includes(breakpoint)) {
|
||||
this.isSidebarExpanded = false;
|
||||
} else if (!this.isSidebarExpanded) {
|
||||
this.isSidebarExpanded = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<gl-loading-icon v-if="isPipelineLoading" size="lg" class="gl-mt-4" />
|
||||
<div v-else>
|
||||
<ci-header
|
||||
class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
|
||||
:status="bridgeJob.detailedStatus"
|
||||
:time="bridgeJob.createdAt"
|
||||
:user="pipeline.user"
|
||||
:has-sidebar-button="true"
|
||||
:item-name="bridgeName"
|
||||
@clickedSidebarButton="toggleSidebar"
|
||||
/>
|
||||
<bridge-empty-state :downstream-pipeline-path="bridgeJob.downstreamPipeline.path" />
|
||||
<bridge-sidebar
|
||||
v-if="isSidebarExpanded"
|
||||
:bridge-job="bridgeJob"
|
||||
:commit="pipeline.commit"
|
||||
:is-sidebar-expanded="isSidebarExpanded"
|
||||
@toggleSidebar="toggleSidebar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1 +0,0 @@
|
|||
export const SIDEBAR_COLLAPSE_BREAKPOINTS = ['xs', 'sm'];
|
|
@ -1,45 +0,0 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
name: 'BridgeEmptyState',
|
||||
i18n: {
|
||||
title: __('This job triggers a downstream pipeline'),
|
||||
linkBtnText: __('View downstream pipeline'),
|
||||
},
|
||||
components: {
|
||||
GlButton,
|
||||
},
|
||||
inject: {
|
||||
emptyStateIllustrationPath: {
|
||||
type: String,
|
||||
require: true,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
downstreamPipelinePath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
|
||||
<img :src="emptyStateIllustrationPath" />
|
||||
<h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
|
||||
<gl-button
|
||||
v-if="downstreamPipelinePath"
|
||||
class="gl-mt-3"
|
||||
category="secondary"
|
||||
variant="confirm"
|
||||
size="medium"
|
||||
:href="downstreamPipelinePath"
|
||||
>
|
||||
{{ $options.i18n.linkBtnText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</template>
|
|
@ -1,105 +0,0 @@
|
|||
<script>
|
||||
import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
|
||||
import { JOB_SIDEBAR } from '../../constants';
|
||||
import CommitBlock from '../../components/commit_block.vue';
|
||||
|
||||
export default {
|
||||
styles: {
|
||||
width: '290px',
|
||||
},
|
||||
name: 'BridgeSidebar',
|
||||
i18n: {
|
||||
...JOB_SIDEBAR,
|
||||
retryButton: __('Retry'),
|
||||
retryTriggerJob: __('Retry the trigger job'),
|
||||
retryDownstreamPipeline: __('Retry the downstream pipeline'),
|
||||
},
|
||||
sectionClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100', 'gl-py-5'],
|
||||
components: {
|
||||
CommitBlock,
|
||||
GlButton,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
TooltipOnTruncate,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
bridgeJob: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
commit: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
topPosition: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
rootStyle() {
|
||||
return { ...this.$options.styles, top: `${this.topPosition}px` };
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setTopPosition();
|
||||
},
|
||||
methods: {
|
||||
onSidebarButtonClick() {
|
||||
this.$emit('toggleSidebar');
|
||||
},
|
||||
setTopPosition() {
|
||||
const navbarEl = document.querySelector('.js-navbar');
|
||||
|
||||
if (navbarEl) {
|
||||
this.topPosition = navbarEl.getBoundingClientRect().bottom;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<aside
|
||||
class="gl-fixed gl-right-0 gl-px-5 gl-bg-gray-10 gl-h-full gl-border-l-solid gl-border-1 gl-border-gray-100 gl-z-index-200 gl-overflow-hidden"
|
||||
:style="rootStyle"
|
||||
>
|
||||
<div class="gl-py-5 gl-display-flex gl-align-items-center">
|
||||
<tooltip-on-truncate :title="bridgeJob.name" truncate-target="child"
|
||||
><h4 class="gl-mb-0 gl-mr-2 gl-text-truncate">
|
||||
{{ bridgeJob.name }}
|
||||
</h4>
|
||||
</tooltip-on-truncate>
|
||||
<!-- TODO: implement retry actions -->
|
||||
<div
|
||||
v-if="glFeatures.triggerJobRetryAction"
|
||||
class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right"
|
||||
>
|
||||
<gl-dropdown
|
||||
:text="$options.i18n.retryButton"
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
right
|
||||
size="medium"
|
||||
>
|
||||
<gl-dropdown-item>{{ $options.i18n.retryTriggerJob }}</gl-dropdown-item>
|
||||
<gl-dropdown-item>{{ $options.i18n.retryDownstreamPipeline }}</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
<gl-button
|
||||
:aria-label="$options.i18n.toggleSidebar"
|
||||
data-testid="sidebar-expansion-toggle"
|
||||
category="tertiary"
|
||||
class="gl-md-display-none gl-ml-2"
|
||||
icon="chevron-double-lg-right"
|
||||
@click="onSidebarButtonClick"
|
||||
/>
|
||||
</div>
|
||||
<commit-block :commit="commit" :class="$options.sectionClass" />
|
||||
<!-- TODO: show stage dropdown, jobs list -->
|
||||
</aside>
|
||||
</template>
|
|
@ -1,70 +0,0 @@
|
|||
query getPipelineData($fullPath: ID!, $iid: ID!) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
pipeline(iid: $iid) {
|
||||
id
|
||||
iid
|
||||
path
|
||||
sha
|
||||
ref
|
||||
refPath
|
||||
commit {
|
||||
id
|
||||
shortId
|
||||
title
|
||||
webPath
|
||||
}
|
||||
detailedStatus {
|
||||
id
|
||||
icon
|
||||
group
|
||||
}
|
||||
stages {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
jobs {
|
||||
nodes {
|
||||
id
|
||||
createdAt
|
||||
name
|
||||
scheduledAt
|
||||
startedAt
|
||||
status
|
||||
triggered
|
||||
detailedStatus {
|
||||
id
|
||||
detailsPath
|
||||
icon
|
||||
group
|
||||
text
|
||||
tooltip
|
||||
}
|
||||
downstreamPipeline {
|
||||
id
|
||||
path
|
||||
}
|
||||
stage {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
user {
|
||||
id
|
||||
avatarUrl
|
||||
name
|
||||
username
|
||||
webPath
|
||||
webUrl
|
||||
status {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import BridgeApp from './bridge/app.vue';
|
||||
import JobApp from './components/job_app.vue';
|
||||
import createStore from './store';
|
||||
|
||||
|
@ -51,43 +48,7 @@ const initializeJobPage = (element) => {
|
|||
});
|
||||
};
|
||||
|
||||
const initializeBridgePage = (el) => {
|
||||
const {
|
||||
buildId,
|
||||
downstreamPipelinePath,
|
||||
emptyStateIllustrationPath,
|
||||
pipelineIid,
|
||||
projectFullPath,
|
||||
} = el.dataset;
|
||||
|
||||
Vue.use(VueApollo);
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
apolloProvider,
|
||||
provide: {
|
||||
buildId,
|
||||
downstreamPipelinePath,
|
||||
emptyStateIllustrationPath,
|
||||
pipelineIid,
|
||||
projectFullPath,
|
||||
},
|
||||
render(h) {
|
||||
return h(BridgeApp);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const jobElement = document.getElementById('js-job-page');
|
||||
const bridgeElement = document.getElementById('js-bridge-page');
|
||||
|
||||
if (jobElement) {
|
||||
initializeJobPage(jobElement);
|
||||
} else {
|
||||
initializeBridgePage(bridgeElement);
|
||||
}
|
||||
initializeJobPage(jobElement);
|
||||
};
|
||||
|
|
|
@ -14,6 +14,8 @@ export default {
|
|||
LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'),
|
||||
TimeToRestoreServiceCharts: () =>
|
||||
import('ee_component/dora/components/time_to_restore_service_charts.vue'),
|
||||
ChangeFailureRateCharts: () =>
|
||||
import('ee_component/dora/components/change_failure_rate_charts.vue'),
|
||||
ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'),
|
||||
},
|
||||
piplelinesTabEvent: 'p_analytics_ci_cd_pipelines',
|
||||
|
@ -40,7 +42,12 @@ export default {
|
|||
const chartsToShow = ['pipelines'];
|
||||
|
||||
if (this.shouldRenderDoraCharts) {
|
||||
chartsToShow.push('deployment-frequency', 'lead-time', 'time-to-restore-service');
|
||||
chartsToShow.push(
|
||||
'deployment-frequency',
|
||||
'lead-time',
|
||||
'time-to-restore-service',
|
||||
'change-failure-rate',
|
||||
);
|
||||
}
|
||||
|
||||
if (this.shouldRenderQualitySummary) {
|
||||
|
@ -105,6 +112,12 @@ export default {
|
|||
>
|
||||
<time-to-restore-service-charts />
|
||||
</gl-tab>
|
||||
<gl-tab
|
||||
:title="s__('DORA4Metrics|Change failure rate')"
|
||||
data-testid="change-failure-rate-tab"
|
||||
>
|
||||
<change-failure-rate-charts />
|
||||
</gl-tab>
|
||||
</template>
|
||||
<gl-tab v-if="shouldRenderQualitySummary" :title="s__('QualitySummary|Project quality')">
|
||||
<project-quality-summary />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlButton, GlSprintf, GlSafeHtmlDirective } from '@gitlab/ui';
|
||||
import { GlButton, GlSprintf, GlSafeHtmlDirective, GlTooltipDirective } from '@gitlab/ui';
|
||||
import gitlabLogo from '@gitlab/svgs/dist/illustrations/gitlab_logo.svg';
|
||||
import { s__, __ } from '~/locale';
|
||||
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
|
||||
|
@ -29,6 +29,7 @@ export default {
|
|||
},
|
||||
directives: {
|
||||
safeHtml: GlSafeHtmlDirective,
|
||||
tooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [Tracking.mixin()],
|
||||
i18n: {
|
||||
|
@ -92,7 +93,7 @@ export default {
|
|||
>
|
||||
<template #default="{ dismiss }">
|
||||
<aside
|
||||
class="gl-fixed gl-bottom-0 gl-right-0 gl-z-index-9999 gl-p-5"
|
||||
class="mr-experience-survey-wrapper gl-fixed gl-bottom-0 gl-right-0 gl-p-5"
|
||||
:aria-label="$options.i18n.survey"
|
||||
>
|
||||
<transition name="survey-slide-up">
|
||||
|
@ -101,6 +102,7 @@ export default {
|
|||
class="mr-experience-survey-body gl-relative gl-display-flex gl-flex-direction-column gl-bg-white gl-p-5 gl-border gl-rounded-base"
|
||||
>
|
||||
<gl-button
|
||||
v-tooltip="$options.i18n.close"
|
||||
:aria-label="$options.i18n.close"
|
||||
variant="default"
|
||||
category="tertiary"
|
||||
|
|
|
@ -350,6 +350,13 @@ $comparison-empty-state-height: 62px;
|
|||
}
|
||||
}
|
||||
|
||||
.mr-experience-survey-wrapper {
|
||||
// setting this explicitly because:
|
||||
// diff-files-holder has z-index 203
|
||||
// z-index 9999 utility class breaks tooltips
|
||||
z-index: 210;
|
||||
}
|
||||
|
||||
.mr-experience-survey-body {
|
||||
width: 300px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
|
|
|
@ -22,10 +22,6 @@ class Projects::JobsController < Projects::ApplicationController
|
|||
before_action :push_jobs_table_vue_search, only: [:index]
|
||||
before_action :reject_if_build_artifacts_size_refreshing!, only: [:erase]
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:trigger_job_retry_action, @project)
|
||||
end
|
||||
|
||||
layout 'project'
|
||||
|
||||
feature_category :continuous_integration
|
||||
|
|
|
@ -436,7 +436,7 @@ class IssuableFinder
|
|||
elsif not_params.filter_by_started_milestone?
|
||||
items.joins(:milestone).merge(Milestone.not_started)
|
||||
else
|
||||
items.without_particular_milestone(not_params[:milestone_title])
|
||||
items.without_particular_milestones(not_params[:milestone_title])
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -116,19 +116,16 @@ module EmailsHelper
|
|||
end
|
||||
end
|
||||
|
||||
# "You are receiving this email because #{reason} on #{gitlab_host}."
|
||||
def notification_reason_text(reason)
|
||||
gitlab_host = Gitlab.config.gitlab.host
|
||||
|
||||
case reason
|
||||
when NotificationReason::OWN_ACTIVITY
|
||||
_("You're receiving this email because of your activity on %{host}.") % { host: gitlab_host }
|
||||
when NotificationReason::ASSIGNED
|
||||
_("You're receiving this email because you have been assigned an item on %{host}.") % { host: gitlab_host }
|
||||
when NotificationReason::MENTIONED
|
||||
_("You're receiving this email because you have been mentioned on %{host}.") % { host: gitlab_host }
|
||||
# "You are receiving this email because ... on #{host}. ..."
|
||||
def notification_reason_text(reason: nil, show_manage_notifications_link: false, show_help_link: false, manage_label_subscriptions_url: nil, unsubscribe_url: nil, format: :text)
|
||||
if unsubscribe_url && show_manage_notifications_link && show_help_link
|
||||
notification_reason_text_with_unsubscribe_and_manage_notifications_and_help_links(reason: reason, unsubscribe_url: unsubscribe_url, format: format)
|
||||
elsif !reason && manage_label_subscriptions_url && show_help_link
|
||||
notification_reason_text_with_manage_label_subscriptions_and_help_links(manage_label_subscriptions_url: manage_label_subscriptions_url, format: format)
|
||||
elsif show_manage_notifications_link && show_help_link
|
||||
notification_reason_text_with_manage_notifications_and_help_links(reason: reason, format: format)
|
||||
else
|
||||
_("You're receiving this email because of your account on %{host}.") % { host: gitlab_host }
|
||||
notification_reason_text_without_links(reason: reason, format: format)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -259,9 +256,7 @@ module EmailsHelper
|
|||
end
|
||||
|
||||
def instance_access_request_text(user, format: nil)
|
||||
gitlab_host = Gitlab.config.gitlab.host
|
||||
|
||||
_('%{username} has asked for a GitLab account on your instance %{host}:') % { username: sanitize_name(user.name), host: gitlab_host }
|
||||
_('%{username} has asked for a GitLab account on your instance %{host}:').html_safe % { username: sanitize_name(user.name), host: gitlab_host_link(format) }
|
||||
end
|
||||
|
||||
def instance_access_request_link(user, format: nil)
|
||||
|
@ -325,6 +320,75 @@ module EmailsHelper
|
|||
def email_header_and_footer_enabled?
|
||||
current_appearance&.email_header_and_footer_enabled?
|
||||
end
|
||||
|
||||
def gitlab_host_link(format)
|
||||
case format
|
||||
when :html
|
||||
generate_link(Gitlab.config.gitlab.host, Gitlab.config.gitlab.url)
|
||||
when :text
|
||||
Gitlab.config.gitlab.host
|
||||
end
|
||||
end
|
||||
|
||||
def notification_reason_text_with_unsubscribe_and_manage_notifications_and_help_links(reason:, unsubscribe_url:, format:)
|
||||
unsubscribe_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: unsubscribe_url }
|
||||
unsubscribe_link_end = '</a>'.html_safe
|
||||
|
||||
manage_notifications_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="mng-notif-link">'.html_safe % { url: profile_notifications_url }
|
||||
manage_notifications_link_end = '</a>'.html_safe
|
||||
|
||||
help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="help-link">'.html_safe % { url: help_url }
|
||||
help_link_end = '</a>'.html_safe
|
||||
|
||||
case reason
|
||||
when NotificationReason::OWN_ACTIVITY
|
||||
_("You're receiving this email because of your activity on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), unsubscribe_link_start: unsubscribe_link_start, unsubscribe_link_end: unsubscribe_link_end, manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end }
|
||||
when NotificationReason::ASSIGNED
|
||||
_("You're receiving this email because you have been assigned an item on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), unsubscribe_link_start: unsubscribe_link_start, unsubscribe_link_end: unsubscribe_link_end, manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end }
|
||||
when NotificationReason::MENTIONED
|
||||
_("You're receiving this email because you have been mentioned on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), unsubscribe_link_start: unsubscribe_link_start, unsubscribe_link_end: unsubscribe_link_end, manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end }
|
||||
else
|
||||
_("You're receiving this email because of your account on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), unsubscribe_link_start: unsubscribe_link_start, unsubscribe_link_end: unsubscribe_link_end, manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end }
|
||||
end
|
||||
end
|
||||
|
||||
def notification_reason_text_with_manage_label_subscriptions_and_help_links(manage_label_subscriptions_url:, format:)
|
||||
manage_label_subscriptions_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="mng-notif-link">'.html_safe % { url: manage_label_subscriptions_url }
|
||||
manage_label_subscriptions_link_end = '</a>'.html_safe
|
||||
|
||||
help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="help-link">'.html_safe % { url: help_url }
|
||||
help_link_end = '</a>'.html_safe
|
||||
|
||||
_("You're receiving this email because of your account on %{host}. %{manage_label_subscriptions_link_start}Manage label subscriptions%{manage_label_subscriptions_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), manage_label_subscriptions_link_start: manage_label_subscriptions_link_start, manage_label_subscriptions_link_end: manage_label_subscriptions_link_end, help_link_start: help_link_start, help_link_end: help_link_end }
|
||||
end
|
||||
|
||||
def notification_reason_text_with_manage_notifications_and_help_links(reason:, format:)
|
||||
manage_notifications_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="mng-notif-link">'.html_safe % { url: profile_notifications_url }
|
||||
manage_notifications_link_end = '</a>'.html_safe
|
||||
|
||||
help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="help-link">'.html_safe % { url: help_url }
|
||||
help_link_end = '</a>'.html_safe
|
||||
|
||||
case reason
|
||||
when NotificationReason::MENTIONED
|
||||
_("You're receiving this email because you have been mentioned on %{host}. %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end }
|
||||
else
|
||||
_("You're receiving this email because of your account on %{host}. %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}").html_safe % { host: gitlab_host_link(format), manage_notifications_link_start: manage_notifications_link_start, manage_notifications_link_end: manage_notifications_link_end, help_link_start: help_link_start, help_link_end: help_link_end }
|
||||
end
|
||||
end
|
||||
|
||||
def notification_reason_text_without_links(reason:, format:)
|
||||
case reason
|
||||
when NotificationReason::OWN_ACTIVITY
|
||||
_("You're receiving this email because of your activity on %{host}.").html_safe % { host: gitlab_host_link(format) }
|
||||
when NotificationReason::ASSIGNED
|
||||
_("You're receiving this email because you have been assigned an item on %{host}.").html_safe % { host: gitlab_host_link(format) }
|
||||
when NotificationReason::MENTIONED
|
||||
_("You're receiving this email because you have been mentioned on %{host}.").html_safe % { host: gitlab_host_link(format) }
|
||||
else
|
||||
_("You're receiving this email because of your account on %{host}.").html_safe % { host: gitlab_host_link(format) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
EmailsHelper.prepend_mod_with('EmailsHelper')
|
||||
|
|
|
@ -16,7 +16,7 @@ module Milestoneable
|
|||
|
||||
scope :any_milestone, -> { where.not(milestone_id: nil) }
|
||||
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
|
||||
scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }
|
||||
scope :without_particular_milestones, ->(titles) { left_outer_joins(:milestone).where("milestones.title NOT IN (?) OR milestone_id IS NULL", titles) }
|
||||
scope :any_release, -> { joins_milestone_releases }
|
||||
scope :with_release, -> (tag, project_id) { joins_milestone_releases.where( milestones: { releases: { tag: tag, project_id: project_id } } ) }
|
||||
scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) }
|
||||
|
|
|
@ -15,7 +15,12 @@ class MergeRequestDiffFile < ApplicationRecord
|
|||
end
|
||||
|
||||
def utf8_diff
|
||||
fetched_diff = diff
|
||||
fetched_diff = if Feature.enabled?(:externally_stored_diffs_caching_export) &&
|
||||
merge_request_diff&.stored_externally?
|
||||
diff_export
|
||||
else
|
||||
diff
|
||||
end
|
||||
|
||||
return '' if fetched_diff.blank?
|
||||
|
||||
|
@ -46,14 +51,13 @@ class MergeRequestDiffFile < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method is meant to be used during Project Export.
|
||||
# It is identical to the behaviour in #diff & #utf8_diff with the only
|
||||
# It is identical to the behaviour in #diff with the only
|
||||
# difference of caching externally stored diffs on local disk in
|
||||
# temp storage location in order to improve diff export performance.
|
||||
def diff_export
|
||||
return utf8_diff unless Feature.enabled?(:externally_stored_diffs_caching_export)
|
||||
return utf8_diff unless merge_request_diff&.stored_externally?
|
||||
|
||||
content = merge_request_diff.cached_external_diff do |file|
|
||||
file.seek(external_diff_offset)
|
||||
|
||||
|
@ -69,14 +73,17 @@ class MergeRequestDiffFile < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
if content.respond_to?(:encoding)
|
||||
content = encode_utf8(content)
|
||||
end
|
||||
|
||||
return '' if content.blank?
|
||||
|
||||
content
|
||||
rescue StandardError
|
||||
utf8_diff
|
||||
rescue StandardError => e
|
||||
log_payload = {
|
||||
message: 'Cached external diff export failed',
|
||||
merge_request_diff_file_id: id,
|
||||
merge_request_diff_id: merge_request_diff&.id
|
||||
}
|
||||
|
||||
Gitlab::ExceptionLogFormatter.format!(e, log_payload)
|
||||
Gitlab::AppLogger.warn(log_payload)
|
||||
|
||||
diff
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Terraform
|
||||
class ModuleVersionPresenter < Gitlab::View::Presenter::Simple
|
||||
attr_accessor :package, :system
|
||||
|
||||
def initialize(package, system)
|
||||
@package = package
|
||||
@system = system
|
||||
end
|
||||
|
||||
def name
|
||||
package.name
|
||||
end
|
||||
|
||||
def provider
|
||||
system
|
||||
end
|
||||
|
||||
def providers
|
||||
[
|
||||
provider
|
||||
]
|
||||
end
|
||||
|
||||
def root
|
||||
{
|
||||
'dependencies' => []
|
||||
}
|
||||
end
|
||||
|
||||
def source
|
||||
package&.project&.web_url
|
||||
end
|
||||
|
||||
def submodules
|
||||
[]
|
||||
end
|
||||
|
||||
def version
|
||||
package.version
|
||||
end
|
||||
|
||||
def versions
|
||||
[
|
||||
version
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,7 @@
|
|||
- expanded = integration_expanded?('eks_')
|
||||
%section.settings.as-eks.no-animate#js-eks-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Amazon EKS')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= s_('ExternalAuthorization|External authorization')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
%section.settings.no-animate#js-floc-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= s_('FloC|Federated Learning of Cohorts')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
%section.settings.no-animate#js-gitpod-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Gitpod')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
%section.settings.no-animate#js-jira_connect-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= s_('JiraConnect|GitLab for Jira App')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- expanded = integration_expanded?('kroki_')
|
||||
%section.settings.as-kroki.no-animate#js-kroki-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Kroki')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- expanded = integration_expanded?('mailgun_')
|
||||
%section.settings.as-mailgun.no-animate#js-mailgun-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Mailgun')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- expanded = integration_expanded?('plantuml_')
|
||||
%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('PlantUML')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- expanded = integration_expanded?('snowplow_')
|
||||
%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded), data: { qa_selector: 'snowplow_settings_content' } }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Snowplow')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
%section.settings.as-sourcegraph.no-animate#js-sourcegraph-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Sourcegraph')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
- expanded = integration_expanded?('hide_third_party_')
|
||||
%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Customer experience improvement and third-party offers')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Visibility and access controls')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
@ -15,7 +15,7 @@
|
|||
|
||||
%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'account_and_limit_settings_content' } }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Account and limit')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
@ -26,7 +26,7 @@
|
|||
|
||||
%section.settings.as-diff-limits.no-animate#js-merge-request-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Diff limits')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
@ -37,7 +37,7 @@
|
|||
|
||||
%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'sign_up_restrictions_settings_content' } }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Sign-up restrictions')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
@ -48,7 +48,7 @@
|
|||
|
||||
%section.settings.as-signin.no-animate#js-signin-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Sign-in restrictions')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
@ -60,7 +60,7 @@
|
|||
|
||||
%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Terms of Service and Privacy Policy')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
@ -74,7 +74,7 @@
|
|||
|
||||
%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Web terminal')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
@ -86,7 +86,7 @@
|
|||
|
||||
%section.settings.no-animate#js-web-ide-settings{ class: ('expanded' if expanded_by_default?) }
|
||||
.settings-header
|
||||
%h4
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
|
||||
= _('Web IDE')
|
||||
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
|
||||
= expanded_by_default? ? _('Collapse') : _('Expand')
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
%td
|
||||
%img.footer-logo{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png') }
|
||||
%div
|
||||
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, class: 'mng-notif-link')
|
||||
- help_link = link_to(_("Help"), help_url, class: 'help-link')
|
||||
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
|
||||
= notification_reason_text(show_manage_notifications_link: true, show_help_link: true, format: :html)
|
||||
|
||||
= render 'layouts/mailer'
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<%= yield -%>
|
||||
|
||||
-- <%# signature marker %>
|
||||
<%= _("You're receiving this email because of your account on %{host}.") % { host: Gitlab.config.gitlab.host } %>
|
||||
<%= notification_reason_text %>
|
||||
<%= render_if_exists 'layouts/mailer/additional_text' %>
|
||||
|
||||
<%= text_footer_message %>
|
||||
|
|
|
@ -26,16 +26,7 @@
|
|||
- else
|
||||
#{link_to _("View it on GitLab"), @target_url}.
|
||||
%br
|
||||
-# Don't link the host in the line below, one link in the email is easier to quickly click than two.
|
||||
= notification_reason_text(@reason)
|
||||
If you'd like to receive fewer emails, you can
|
||||
- if @labels_url
|
||||
adjust your #{link_to 'label subscriptions', @labels_url}.
|
||||
- else
|
||||
- if @unsubscribe_url
|
||||
= link_to "unsubscribe", @unsubscribe_url
|
||||
from this thread or
|
||||
adjust your notification settings.
|
||||
= notification_reason_text(reason: @reason, show_manage_notifications_link: !@labels_url, show_help_link: true, manage_label_subscriptions_url: @labels_url, unsubscribe_url: @unsubscribe_url, format: :html)
|
||||
|
||||
= email_action @target_url
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<% end -%>
|
||||
<% end -%>
|
||||
|
||||
<%= notification_reason_text(@reason) %>
|
||||
<%= notification_reason_text(reason: @reason) %>
|
||||
<%= render_if_exists 'layouts/mailer/additional_text' %>
|
||||
|
||||
<%= text_footer_message -%>
|
||||
|
|
|
@ -152,6 +152,4 @@
|
|||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
|
||||
%img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/
|
||||
%div
|
||||
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
|
||||
- help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
|
||||
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
|
||||
= notification_reason_text(show_manage_notifications_link: true, show_help_link: true, format: :html)
|
||||
|
|
|
@ -148,6 +148,4 @@
|
|||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
|
||||
%img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }
|
||||
%div
|
||||
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
|
||||
- help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
|
||||
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
|
||||
= notification_reason_text(show_manage_notifications_link: true, show_help_link: true, format: :html)
|
||||
|
|
|
@ -151,6 +151,4 @@
|
|||
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
|
||||
%img{ alt: "GitLab", src: image_url('mailers/gitlab_logo_black_text.png'), style: "display:block;margin:0 auto 1em;", width: "90" }/
|
||||
%div
|
||||
- manage_notifications_link = link_to(_("Manage all notifications"), profile_notifications_url, style: "color:#3777b0;text-decoration:none;")
|
||||
- help_link = link_to(_("Help"), help_url, style: "color:#3777b0;text-decoration:none;")
|
||||
= _("You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}").html_safe % { host: Gitlab.config.gitlab.host, manage_notifications_link: manage_notifications_link, help_link: help_link }
|
||||
= notification_reason_text(show_manage_notifications_link: true, show_help_link: true, format: :html)
|
||||
|
|
|
@ -7,7 +7,4 @@
|
|||
|
||||
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
|
||||
|
||||
- if @build.is_a? ::Ci::Build
|
||||
#js-job-page{ data: jobs_data }
|
||||
- else
|
||||
#js-bridge-page{ data: bridge_data(@build, @project) }
|
||||
#js-job-page{ data: jobs_data }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
= link_to root_path do
|
||||
= render 'shared/logo.svg'
|
||||
= render partial: 'shared/logo', formats: :svg
|
||||
%h1= _('Offline')
|
||||
.container
|
||||
%h3= _('You are currently offline, or the GitLab instance is not reachable.')
|
||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363186
|
|||
milestone: '15.1'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
---
|
||||
name: trigger_job_retry_action
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77951
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/349966
|
||||
milestone: '14.7'
|
||||
type: development
|
||||
group: group::pipeline authoring
|
||||
default_enabled: false
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
table_name: namespace_bans
|
||||
classes:
|
||||
- NamespaceBan
|
||||
feature_categories:
|
||||
- instance_resiliency
|
||||
description: Contains users banned from namespaces
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91271
|
||||
milestone: "15.2"
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateNamespaceBans < Gitlab::Database::Migration[2.0]
|
||||
UNIQUE_INDEX_NAME = 'index_namespace_bans_on_namespace_id_and_user_id'
|
||||
|
||||
def change
|
||||
create_table :namespace_bans do |t|
|
||||
t.bigint :namespace_id, null: false
|
||||
t.bigint :user_id, null: false, index: true
|
||||
t.timestamps_with_timezone
|
||||
|
||||
t.index [:namespace_id, :user_id], unique: true, name: UNIQUE_INDEX_NAME
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddNamespaceBansNamespaceIdForeignKey < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :namespace_bans, :namespaces, column: :namespace_id, on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :namespace_bans, column: :namespace_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddNamespaceBansUserIdForeignKey < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :namespace_bans, :users, column: :user_id, on_delete: :cascade
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
remove_foreign_key :namespace_bans, column: :user_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class IncreaseWebauthnXidLength < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
new_constraint_name = check_constraint_name(:webauthn_registrations, :credential_xid, 'max_length_v3')
|
||||
add_text_limit :webauthn_registrations, :credential_xid, 1364, constraint_name: new_constraint_name
|
||||
|
||||
prev_constraint_name = check_constraint_name(:webauthn_registrations, :credential_xid, 'max_length_v2')
|
||||
remove_text_limit :webauthn_registrations, :credential_xid, constraint_name: prev_constraint_name
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op: Danger of failling if there are records with length(credential_xid) > 1364
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndicesOnSecurityScansInfoColumn < Gitlab::Database::Migration[2.0]
|
||||
INDEX_NAME_ON_ERRORS = :index_security_scans_on_length_of_errors
|
||||
INDEX_NAME_ON_WARNINGS = :index_security_scans_on_length_of_warnings
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index(
|
||||
:security_scans,
|
||||
"pipeline_id, jsonb_array_length(COALESCE((security_scans.info -> 'errors'::text), '[]'::jsonb))",
|
||||
name: INDEX_NAME_ON_ERRORS
|
||||
)
|
||||
|
||||
add_concurrent_index(
|
||||
:security_scans,
|
||||
"pipeline_id, jsonb_array_length(COALESCE((security_scans.info -> 'warnings'::text), '[]'::jsonb))",
|
||||
name: INDEX_NAME_ON_WARNINGS
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :security_scans, INDEX_NAME_ON_ERRORS
|
||||
remove_concurrent_index_by_name :security_scans, INDEX_NAME_ON_WARNINGS
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
75027a5b09491b156837707af20406b2672d5ee3ce2272ecf1496e98da2861bf
|
|
@ -0,0 +1 @@
|
|||
90b9b47ef3671b73117205264589f895a083b0d00db00e684b25e60673d2e840
|
|
@ -0,0 +1 @@
|
|||
d64a9c41376bbb3bc2c9df846668b1a67b0bed1b1410d97dba17c19a2f322b38
|
|
@ -0,0 +1 @@
|
|||
2f5e08212b2f733ce5812d7154879768532e31e642b647648d1c03fd4ddf8b13
|
|
@ -0,0 +1 @@
|
|||
ea387b35bfb7f15a036aca9413b8fd15ede6b16048fa9e9be5a62b9e21ca362d
|
|
@ -17493,6 +17493,23 @@ CREATE TABLE namespace_aggregation_schedules (
|
|||
namespace_id integer NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE namespace_bans (
|
||||
id bigint NOT NULL,
|
||||
namespace_id bigint NOT NULL,
|
||||
user_id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE namespace_bans_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE namespace_bans_id_seq OWNED BY namespace_bans.id;
|
||||
|
||||
CREATE TABLE namespace_ci_cd_settings (
|
||||
namespace_id bigint NOT NULL,
|
||||
allow_stale_runner_pruning boolean DEFAULT false NOT NULL
|
||||
|
@ -22447,7 +22464,7 @@ CREATE TABLE webauthn_registrations (
|
|||
public_key text NOT NULL,
|
||||
u2f_registration_id integer,
|
||||
CONSTRAINT check_2f02e74321 CHECK ((char_length(name) <= 255)),
|
||||
CONSTRAINT check_e54008d9ce CHECK ((char_length(credential_xid) <= 340))
|
||||
CONSTRAINT check_f5ab2b551a CHECK ((char_length(credential_xid) <= 1364))
|
||||
);
|
||||
|
||||
CREATE SEQUENCE webauthn_registrations_id_seq
|
||||
|
@ -23223,6 +23240,8 @@ ALTER TABLE ONLY milestones ALTER COLUMN id SET DEFAULT nextval('milestones_id_s
|
|||
|
||||
ALTER TABLE ONLY namespace_admin_notes ALTER COLUMN id SET DEFAULT nextval('namespace_admin_notes_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY namespace_bans ALTER COLUMN id SET DEFAULT nextval('namespace_bans_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY namespace_statistics ALTER COLUMN id SET DEFAULT nextval('namespace_statistics_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY namespaces ALTER COLUMN id SET DEFAULT nextval('namespaces_id_seq'::regclass);
|
||||
|
@ -25234,6 +25253,9 @@ ALTER TABLE ONLY namespace_admin_notes
|
|||
ALTER TABLE ONLY namespace_aggregation_schedules
|
||||
ADD CONSTRAINT namespace_aggregation_schedules_pkey PRIMARY KEY (namespace_id);
|
||||
|
||||
ALTER TABLE ONLY namespace_bans
|
||||
ADD CONSTRAINT namespace_bans_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY namespace_ci_cd_settings
|
||||
ADD CONSTRAINT namespace_ci_cd_settings_pkey PRIMARY KEY (namespace_id);
|
||||
|
||||
|
@ -28780,6 +28802,10 @@ CREATE INDEX index_namespace_admin_notes_on_namespace_id ON namespace_admin_note
|
|||
|
||||
CREATE UNIQUE INDEX index_namespace_aggregation_schedules_on_namespace_id ON namespace_aggregation_schedules USING btree (namespace_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_namespace_bans_on_namespace_id_and_user_id ON namespace_bans USING btree (namespace_id, user_id);
|
||||
|
||||
CREATE INDEX index_namespace_bans_on_user_id ON namespace_bans USING btree (user_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id);
|
||||
|
@ -29540,6 +29566,10 @@ CREATE INDEX index_security_scans_on_created_at ON security_scans USING btree (c
|
|||
|
||||
CREATE INDEX index_security_scans_on_date_created_at_and_id ON security_scans USING btree (date(timezone('UTC'::text, created_at)), id);
|
||||
|
||||
CREATE INDEX index_security_scans_on_length_of_errors ON security_scans USING btree (pipeline_id, jsonb_array_length(COALESCE((info -> 'errors'::text), '[]'::jsonb)));
|
||||
|
||||
CREATE INDEX index_security_scans_on_length_of_warnings ON security_scans USING btree (pipeline_id, jsonb_array_length(COALESCE((info -> 'warnings'::text), '[]'::jsonb)));
|
||||
|
||||
CREATE INDEX index_security_scans_on_pipeline_id ON security_scans USING btree (pipeline_id);
|
||||
|
||||
CREATE INDEX index_security_scans_on_project_id ON security_scans USING btree (project_id);
|
||||
|
@ -31777,6 +31807,9 @@ ALTER TABLE ONLY protected_environment_approval_rules
|
|||
ALTER TABLE ONLY ci_pipeline_schedule_variables
|
||||
ADD CONSTRAINT fk_41c35fda51 FOREIGN KEY (pipeline_schedule_id) REFERENCES ci_pipeline_schedules(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY namespace_bans
|
||||
ADD CONSTRAINT fk_4275fbb1d7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY geo_event_log
|
||||
ADD CONSTRAINT fk_42c3b54bed FOREIGN KEY (cache_invalidation_event_id) REFERENCES geo_cache_invalidation_events(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -32164,6 +32197,9 @@ ALTER TABLE ONLY deployments
|
|||
ALTER TABLE ONLY routes
|
||||
ADD CONSTRAINT fk_bb2e5b8968 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY namespace_bans
|
||||
ADD CONSTRAINT fk_bcc024eef2 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY gitlab_subscriptions
|
||||
ADD CONSTRAINT fk_bd0c4019c3 FOREIGN KEY (hosted_plan_id) REFERENCES plans(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -347,6 +347,7 @@ Some basic Ruby runtime metrics are available:
|
|||
|:---------------------------------------- |:--------- |:----- |:----------- |
|
||||
| `ruby_gc_duration_seconds` | Counter | 11.1 | Time spent by Ruby in GC |
|
||||
| `ruby_gc_stat_...` | Gauge | 11.1 | Various metrics from [GC.stat](https://ruby-doc.org/core-2.6.5/GC.html#method-c-stat) |
|
||||
| `ruby_gc_stat_ext_heap_fragmentation` | Gauge | 15.2 | Degree of Ruby heap fragmentation as live objects versus eden slots (range 0 to 1) |
|
||||
| `ruby_file_descriptors` | Gauge | 11.1 | File descriptors per process |
|
||||
| `ruby_sampler_duration_seconds` | Counter | 11.1 | Time spent collecting stats |
|
||||
| `ruby_process_cpu_seconds_total` | Gauge | 12.0 | Total amount of CPU time per process |
|
||||
|
|
|
@ -3530,12 +3530,12 @@ in that container.
|
|||
#### `service:pull_policy`
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21619) in GitLab 15.1 [with a flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`. Disabled by default.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/363186) in GitLab 15.2.
|
||||
> - Requires GitLab Runner 15.1 or later.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||
ask an administrator to [enable the feature flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`.
|
||||
The feature is not ready for production use.
|
||||
On self-managed GitLab, by default this feature is available. To hide the feature,
|
||||
ask an administrator to [disable the feature flag](../../administration/feature_flags.md) named `ci_docker_image_pull_policy`.
|
||||
|
||||
The pull policy that the runner uses to fetch the Docker image.
|
||||
|
||||
|
|
|
@ -360,6 +360,41 @@ Ongoing improvements to report validation are tracked [in this epic](https://git
|
|||
In the meantime, you can see which versions are supported in the
|
||||
[source code](https://gitlab.com/gitlab-org/gitlab/-/blob/08dd756429731a0cca1e27ca9d59eea226398a7d/lib/gitlab/ci/parsers/security/validators/schema_validator.rb#L9-27).
|
||||
|
||||
#### Validate locally
|
||||
|
||||
Before running your analyzer in GitLab, you should validate the report produced by your analyzer to
|
||||
ensure it complies with the declared schema version.
|
||||
|
||||
Use the script below to validate JSON files against a given schema.
|
||||
|
||||
```ruby
|
||||
require 'bundler/inline'
|
||||
|
||||
gemfile do
|
||||
source 'https://rubygems.org'
|
||||
gem 'json_schemer'
|
||||
end
|
||||
|
||||
require 'json'
|
||||
require 'pathname'
|
||||
|
||||
raise 'Usage: ruby script.rb <security schema file name> <report file name>' unless ARGV.size == 2
|
||||
|
||||
schema = JSONSchemer.schema(Pathname.new(ARGV[0]))
|
||||
report = JSON.parse(File.open(ARGV[1]).read)
|
||||
schema_validation_errors = schema.validate(report).map { |error| JSONSchemer::Errors.pretty(error) }
|
||||
puts(schema_validation_errors)
|
||||
```
|
||||
|
||||
1. Download the appropriate schema that matches your report type and declared version. For
|
||||
example, you can find version `14.0.6` of the `container_scanning` report schema at
|
||||
`https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/raw/v14.0.6/dist/container-scanning-report-format.json?inline=false`.
|
||||
1. Save the Ruby script above in a file, for example, `validate.rb`.
|
||||
1. Run the script, passing the schema and report file names as arguments in order. For example:
|
||||
1. Using your local Ruby interpreter: `ruby validate.rb container-scanning-format_14-0-6.json gl-container-scanning-report.json`.
|
||||
1. Using Docker: `docker run -it --rm -v $(pwd):/ci ruby:3-slim ruby /ci/validate.rb /ci/container-scanning-format_14-0-6.json /ci/gl-container-scanning-report.json`
|
||||
1. Validation errors are shown on the screen. You must resolve these errors before GitLab can ingest your report.
|
||||
|
||||
### Report Fields
|
||||
|
||||
#### Version
|
||||
|
|
|
@ -78,7 +78,7 @@ To view the lead time for changes chart:
|
|||
|
||||
![Lead time](img/lead_time_chart_v13_11.png)
|
||||
|
||||
## View time to restore service chart **(PREMIUM)**
|
||||
## View time to restore service chart **(ULTIMATE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/356959) in GitLab 15.1
|
||||
|
||||
|
@ -93,3 +93,17 @@ To view the time to restore service chart:
|
|||
1. Select the **Time to restore service** tab.
|
||||
|
||||
![Lead time](img/time_to_restore_service_charts_v15_1.png)
|
||||
|
||||
## View change failure rate chart **(ULTIMATE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/357072) in GitLab 15.2
|
||||
|
||||
The change failure rate chart shows information about the percentage of deployments that cause an incident in a production environment. This chart is available for groups and projects.
|
||||
|
||||
Change failure rate is one of the four [DORA metrics](index.md#devops-research-and-assessment-dora-key-metrics) that DevOps teams use for measuring excellence in software delivery.
|
||||
|
||||
To view the change failure rate chart:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Analytics > CI/CD Analytics**.
|
||||
1. Select the **Change failure rate** tab.
|
||||
|
|
|
@ -873,6 +873,7 @@ Payload example:
|
|||
},
|
||||
"object_attributes": {
|
||||
"id": 99,
|
||||
"iid": 1,
|
||||
"target_branch": "master",
|
||||
"source_branch": "ms-viewport",
|
||||
"source_project_id": 14,
|
||||
|
@ -884,10 +885,12 @@ Payload example:
|
|||
"milestone_id": null,
|
||||
"state": "opened",
|
||||
"blocking_discussions_resolved": true,
|
||||
"work_in_progress": false,
|
||||
"first_contribution": true,
|
||||
"merge_status": "unchecked",
|
||||
"target_project_id": 14,
|
||||
"iid": 1,
|
||||
"description": "",
|
||||
"url": "http://example.com/diaspora/merge_requests/1",
|
||||
"source": {
|
||||
"name":"Awesome Project",
|
||||
"description":"Aut reprehenderit ut est.",
|
||||
|
@ -930,8 +933,18 @@ Payload example:
|
|||
"email": "gitlabdev@dv6700.(none)"
|
||||
}
|
||||
},
|
||||
"work_in_progress": false,
|
||||
"url": "http://example.com/diaspora/merge_requests/1",
|
||||
"labels": [{
|
||||
"id": 206,
|
||||
"title": "API",
|
||||
"color": "#ffffff",
|
||||
"project_id": 14,
|
||||
"created_at": "2013-12-03T17:15:43Z",
|
||||
"updated_at": "2013-12-03T17:15:43Z",
|
||||
"template": false,
|
||||
"description": "API related issues",
|
||||
"type": "ProjectLabel",
|
||||
"group_id": 41
|
||||
}],
|
||||
"action": "open",
|
||||
"assignee": {
|
||||
"name": "User1",
|
||||
|
@ -990,6 +1003,9 @@ Payload example:
|
|||
}
|
||||
```
|
||||
|
||||
NOTE:
|
||||
The fields `assignee_id`, and `state` are deprecated.
|
||||
|
||||
## Wiki page events
|
||||
|
||||
Wiki page events are triggered when a wiki page is created, updated, or deleted.
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module Entities
|
||||
module Terraform
|
||||
class ModuleVersion < Grape::Entity
|
||||
expose :name
|
||||
expose :provider
|
||||
expose :providers
|
||||
expose :root
|
||||
expose :source
|
||||
expose :submodules
|
||||
expose :version
|
||||
expose :versions
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -151,6 +151,16 @@ module API
|
|||
present_carrierwave_file!(package_file.file)
|
||||
end
|
||||
end
|
||||
|
||||
# This endpoint has to be the last within namespace '*module_version' block
|
||||
# due to how the route matching works in grape
|
||||
# format: false is required, otherwise grape splits the semver version into 2 params:
|
||||
# params[:module_version] and params[:format],
|
||||
# thus leading to an invalid/not found module version
|
||||
get format: false do
|
||||
presenter = ::Terraform::ModuleVersionPresenter.new(package, params[:module_system])
|
||||
present presenter, with: ::API::Entities::Terraform::ModuleVersion
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ module API
|
|||
params do
|
||||
requires :event, type: String, desc: 'The event name that should be tracked'
|
||||
end
|
||||
post 'increment_unique_users' do
|
||||
post 'increment_unique_users', urgency: :low do
|
||||
event_name = params[:event]
|
||||
|
||||
increment_unique_values(event_name, current_user.id)
|
||||
|
|
|
@ -18,7 +18,7 @@ module Gitlab
|
|||
user: user.hook_attrs,
|
||||
project: issuable.project.hook_attrs,
|
||||
object_attributes: issuable_builder.new(issuable).build,
|
||||
labels: issuable.labels.map(&:hook_attrs),
|
||||
labels: issuable.labels_hook_attrs,
|
||||
changes: final_changes(changes.slice(*safe_keys)),
|
||||
# DEPRECATED
|
||||
repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)
|
||||
|
|
|
@ -327,6 +327,7 @@ milestone_releases: :gitlab_main
|
|||
milestones: :gitlab_main
|
||||
namespace_admin_notes: :gitlab_main
|
||||
namespace_aggregation_schedules: :gitlab_main
|
||||
namespace_bans: :gitlab_main
|
||||
namespace_limits: :gitlab_main
|
||||
namespace_package_settings: :gitlab_main
|
||||
namespace_root_storage_statistics: :gitlab_main
|
||||
|
|
|
@ -62,7 +62,8 @@ module Gitlab
|
|||
assignee_id: merge_request.assignee_ids.first, # This key is deprecated
|
||||
labels: merge_request.labels_hook_attrs,
|
||||
state: merge_request.state, # This key is deprecated
|
||||
blocking_discussions_resolved: merge_request.mergeable_discussions_state?
|
||||
blocking_discussions_resolved: merge_request.mergeable_discussions_state?,
|
||||
first_contribution: merge_request.first_contribution?
|
||||
}
|
||||
|
||||
merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)
|
||||
|
|
|
@ -977,7 +977,7 @@ methods:
|
|||
statuses:
|
||||
- :type
|
||||
merge_request_diff_files:
|
||||
- :diff_export
|
||||
- :utf8_diff
|
||||
merge_requests:
|
||||
- :diff_head_sha
|
||||
- :source_branch_sha
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Metrics
|
||||
module Memory
|
||||
extend self
|
||||
|
||||
HEAP_SLOTS_PER_PAGE = GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT]
|
||||
|
||||
def gc_heap_fragmentation(gc_stat = GC.stat)
|
||||
1 - (gc_stat[:heap_live_slots] / (HEAP_SLOTS_PER_PAGE * gc_stat[:heap_eden_pages].to_f))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -39,7 +39,8 @@ module Gitlab
|
|||
process_proportional_memory_bytes: ::Gitlab::Metrics.gauge(metric_name(:process, :proportional_memory_bytes), 'Memory used (PSS)', labels),
|
||||
process_start_time_seconds: ::Gitlab::Metrics.gauge(metric_name(:process, :start_time_seconds), 'Process start time seconds'),
|
||||
sampler_duration: ::Gitlab::Metrics.counter(metric_name(:sampler, :duration_seconds_total), 'Sampler time', labels),
|
||||
gc_duration_seconds: ::Gitlab::Metrics.histogram(metric_name(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS)
|
||||
gc_duration_seconds: ::Gitlab::Metrics.histogram(metric_name(:gc, :duration_seconds), 'GC time', labels, GC_REPORT_BUCKETS),
|
||||
heap_fragmentation: ::Gitlab::Metrics.gauge(metric_name(:gc_stat_ext, :heap_fragmentation), 'Ruby heap fragmentation', labels)
|
||||
}
|
||||
|
||||
GC.stat.keys.each do |key|
|
||||
|
@ -76,8 +77,13 @@ module Gitlab
|
|||
end
|
||||
|
||||
# Collect generic GC stats
|
||||
GC.stat.each do |key, value|
|
||||
metrics[key].set(labels, value)
|
||||
GC.stat.then do |gc_stat|
|
||||
gc_stat.each do |key, value|
|
||||
metrics[key].set(labels, value)
|
||||
end
|
||||
|
||||
# Collect custom GC stats
|
||||
metrics[:heap_fragmentation].set(labels, Memory.gc_heap_fragmentation(gc_stat))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -105,6 +105,8 @@ module UnnestedInFilters
|
|||
# LIMIT 20
|
||||
#
|
||||
def rewrite
|
||||
log_rewrite
|
||||
|
||||
model.from(from)
|
||||
.limit(limit_value)
|
||||
.order(order_values)
|
||||
|
@ -125,6 +127,10 @@ module UnnestedInFilters
|
|||
|
||||
delegate :model, :order_values, :limit_value, :where_values_hash, to: :relation, private: true
|
||||
|
||||
def log_rewrite
|
||||
::Gitlab::AppLogger.info(message: 'Query is being rewritten by `UnnestedInFilters`', model: model.name)
|
||||
end
|
||||
|
||||
def from
|
||||
[value_tables.map(&:to_sql) + [lateral]].join(', ')
|
||||
end
|
||||
|
|
|
@ -6969,6 +6969,9 @@ msgstr ""
|
|||
msgid "CICDAnalytics|All time"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|Change failure rate"
|
||||
msgstr ""
|
||||
|
||||
msgid "CICDAnalytics|Deployment frequency"
|
||||
msgstr ""
|
||||
|
||||
|
@ -23625,9 +23628,6 @@ msgstr ""
|
|||
msgid "Manage access"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage all notifications"
|
||||
msgstr ""
|
||||
|
||||
msgid "Manage applications that can use GitLab as an OAuth provider, and applications that you've authorized to use your account."
|
||||
msgstr ""
|
||||
|
||||
|
@ -32982,12 +32982,6 @@ msgstr ""
|
|||
msgid "Retry migration"
|
||||
msgstr ""
|
||||
|
||||
msgid "Retry the downstream pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "Retry the trigger job"
|
||||
msgstr ""
|
||||
|
||||
msgid "Retry this job"
|
||||
msgstr ""
|
||||
|
||||
|
@ -39678,9 +39672,6 @@ msgstr ""
|
|||
msgid "This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes."
|
||||
msgstr ""
|
||||
|
||||
msgid "This job triggers a downstream pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "This job will automatically run after its timer finishes. Often they are used for incremental roll-out deploys to production environments. When unscheduled it converts into a manual action."
|
||||
msgstr ""
|
||||
|
||||
|
@ -42467,9 +42458,6 @@ msgstr ""
|
|||
msgid "View documentation"
|
||||
msgstr ""
|
||||
|
||||
msgid "View downstream pipeline"
|
||||
msgstr ""
|
||||
|
||||
msgid "View eligible approvers"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44489,18 +44477,36 @@ msgstr ""
|
|||
msgid "You're receiving this email because of your account on %{host}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} · %{help_link}"
|
||||
msgid "You're receiving this email because of your account on %{host}. %{manage_label_subscriptions_link_start}Manage label subscriptions%{manage_label_subscriptions_link_end} · %{help_link_start}Help%{help_link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because of your account on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because of your activity on %{host}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because of your activity on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because you have been assigned an item on %{host}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because you have been assigned an item on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because you have been mentioned on %{host}."
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because you have been mentioned on %{host}. %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "You're receiving this email because you have been mentioned on %{host}. %{unsubscribe_link_start}Unsubscribe%{unsubscribe_link_end} from this thread · %{manage_notifications_link_start}Manage all notifications%{manage_notifications_link_end} · %{help_link_start}Help%{help_link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication."
|
||||
msgstr ""
|
||||
|
||||
|
@ -44930,6 +44936,9 @@ msgstr ""
|
|||
msgid "allowed to fail"
|
||||
msgstr ""
|
||||
|
||||
msgid "already banned from namespace"
|
||||
msgstr ""
|
||||
|
||||
msgid "already being used for another group or project %{timebox_name}."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ require_relative '../lib/prometheus/cleanup_multiproc_dir_service'
|
|||
require_relative '../lib/gitlab/metrics/prometheus'
|
||||
require_relative '../lib/gitlab/metrics'
|
||||
require_relative '../lib/gitlab/metrics/system'
|
||||
require_relative '../lib/gitlab/metrics/memory'
|
||||
require_relative '../lib/gitlab/metrics/samplers/base_sampler'
|
||||
require_relative '../lib/gitlab/metrics/samplers/ruby_sampler'
|
||||
require_relative '../lib/gitlab/metrics/exporter/base_exporter'
|
||||
|
|
|
@ -18,13 +18,13 @@ module QA
|
|||
end
|
||||
|
||||
def set_allow_duplicates_disabled
|
||||
expand_content :package_registry_settings_content do
|
||||
within_element :package_registry_settings_content do
|
||||
click_on_allow_duplicates_button if duplicates_enabled?
|
||||
end
|
||||
end
|
||||
|
||||
def set_allow_duplicates_enabled
|
||||
expand_content :package_registry_settings_content do
|
||||
within_element :package_registry_settings_content do
|
||||
click_on_allow_duplicates_button unless duplicates_enabled?
|
||||
end
|
||||
end
|
||||
|
@ -49,7 +49,7 @@ module QA
|
|||
end
|
||||
|
||||
def has_dependency_proxy_enabled?
|
||||
expand_content :dependency_proxy_settings_content do
|
||||
within_element :dependency_proxy_settings_content do
|
||||
within_element :dependency_proxy_setting_toggle do
|
||||
toggle = find('button.gl-toggle')
|
||||
toggle[:class].include?('is-checked')
|
||||
|
|
|
@ -14,7 +14,7 @@ RSpec.describe 'Unsubscribe links', :sidekiq_might_not_need_inline do
|
|||
let(:mail) { ActionMailer::Base.deliveries.last }
|
||||
let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }
|
||||
let(:header_link) { mail.header['List-Unsubscribe'].to_s[1..-2] } # Strip angle brackets
|
||||
let(:body_link) { body.find_link('unsubscribe')['href'] }
|
||||
let(:body_link) { body.find_link('Unsubscribe')['href'] }
|
||||
|
||||
before do
|
||||
perform_enqueued_jobs { issue }
|
||||
|
|
55
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/single_version.json
vendored
Normal file
55
spec/fixtures/api/schemas/public_api/v4/packages/terraform/modules/v1/single_version.json
vendored
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"provider",
|
||||
"providers",
|
||||
"root",
|
||||
"source",
|
||||
"submodules",
|
||||
"version",
|
||||
"versions"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"dependencies"
|
||||
],
|
||||
"properties": {
|
||||
"dependencies": {
|
||||
"type": "array",
|
||||
"maxItems": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"submodules": {
|
||||
"type": "array",
|
||||
"maxItems": 0
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"versions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
import Vue, { nextTick } from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import getPipelineQuery from '~/jobs/bridge/graphql/queries/pipeline.query.graphql';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import BridgeApp from '~/jobs/bridge/app.vue';
|
||||
import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue';
|
||||
import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue';
|
||||
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
|
||||
import {
|
||||
MOCK_BUILD_ID,
|
||||
MOCK_PIPELINE_IID,
|
||||
MOCK_PROJECT_FULL_PATH,
|
||||
mockPipelineQueryResponse,
|
||||
} from './mock_data';
|
||||
|
||||
describe('Bridge Show Page', () => {
|
||||
let wrapper;
|
||||
let mockApollo;
|
||||
let mockPipelineQuery;
|
||||
|
||||
const createComponent = (options) => {
|
||||
wrapper = shallowMount(BridgeApp, {
|
||||
provide: {
|
||||
buildId: MOCK_BUILD_ID,
|
||||
projectFullPath: MOCK_PROJECT_FULL_PATH,
|
||||
pipelineIid: MOCK_PIPELINE_IID,
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
queries: {
|
||||
pipeline: {
|
||||
loading: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
const createComponentWithApollo = () => {
|
||||
const handlers = [[getPipelineQuery, mockPipelineQuery]];
|
||||
Vue.use(VueApollo);
|
||||
mockApollo = createMockApollo(handlers);
|
||||
|
||||
createComponent({
|
||||
apolloProvider: mockApollo,
|
||||
mocks: {},
|
||||
});
|
||||
};
|
||||
|
||||
const findCiHeader = () => wrapper.findComponent(CiHeader);
|
||||
const findEmptyState = () => wrapper.findComponent(BridgeEmptyState);
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findSidebar = () => wrapper.findComponent(BridgeSidebar);
|
||||
|
||||
beforeEach(() => {
|
||||
mockPipelineQuery = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockPipelineQuery.mockReset();
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('while pipeline query is loading', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders loading icon', () => {
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('after pipeline query is loaded', () => {
|
||||
beforeEach(async () => {
|
||||
mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse);
|
||||
createComponentWithApollo();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
it('query is called with correct variables', async () => {
|
||||
expect(mockPipelineQuery).toHaveBeenCalledTimes(1);
|
||||
expect(mockPipelineQuery).toHaveBeenCalledWith({
|
||||
fullPath: MOCK_PROJECT_FULL_PATH,
|
||||
iid: MOCK_PIPELINE_IID,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders CI header state', () => {
|
||||
expect(findCiHeader().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders empty state', () => {
|
||||
expect(findEmptyState().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders sidebar', () => {
|
||||
expect(findSidebar().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sidebar expansion', () => {
|
||||
beforeEach(async () => {
|
||||
mockPipelineQuery.mockResolvedValue(mockPipelineQueryResponse);
|
||||
createComponentWithApollo();
|
||||
await waitForPromises();
|
||||
});
|
||||
|
||||
describe('on resize', () => {
|
||||
it.each`
|
||||
breakpoint | isSidebarExpanded
|
||||
${'xs'} | ${false}
|
||||
${'sm'} | ${false}
|
||||
${'md'} | ${true}
|
||||
${'lg'} | ${true}
|
||||
${'xl'} | ${true}
|
||||
`(
|
||||
'sets isSidebarExpanded to `$isSidebarExpanded` when the breakpoint is "$breakpoint"',
|
||||
async ({ breakpoint, isSidebarExpanded }) => {
|
||||
jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockReturnValue(breakpoint);
|
||||
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
await nextTick();
|
||||
|
||||
expect(findSidebar().exists()).toBe(isSidebarExpanded);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('toggles expansion on button click', async () => {
|
||||
expect(findSidebar().exists()).toBe(true);
|
||||
|
||||
wrapper.vm.toggleSidebar();
|
||||
await nextTick();
|
||||
|
||||
expect(findSidebar().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,58 +0,0 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import BridgeEmptyState from '~/jobs/bridge/components/empty_state.vue';
|
||||
import { MOCK_EMPTY_ILLUSTRATION_PATH, MOCK_PATH_TO_DOWNSTREAM } from '../mock_data';
|
||||
|
||||
describe('Bridge Empty State', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = ({ downstreamPipelinePath }) => {
|
||||
wrapper = shallowMount(BridgeEmptyState, {
|
||||
provide: {
|
||||
emptyStateIllustrationPath: MOCK_EMPTY_ILLUSTRATION_PATH,
|
||||
},
|
||||
propsData: {
|
||||
downstreamPipelinePath,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findSvg = () => wrapper.find('img');
|
||||
const findTitle = () => wrapper.find('h1');
|
||||
const findLinkBtn = () => wrapper.findComponent(GlButton);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ downstreamPipelinePath: MOCK_PATH_TO_DOWNSTREAM });
|
||||
});
|
||||
|
||||
it('renders illustration', () => {
|
||||
expect(findSvg().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders title', () => {
|
||||
expect(findTitle().exists()).toBe(true);
|
||||
expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title);
|
||||
});
|
||||
|
||||
it('renders CTA button', () => {
|
||||
expect(findLinkBtn().exists()).toBe(true);
|
||||
expect(findLinkBtn().text()).toBe(wrapper.vm.$options.i18n.linkBtnText);
|
||||
expect(findLinkBtn().attributes('href')).toBe(MOCK_PATH_TO_DOWNSTREAM);
|
||||
});
|
||||
});
|
||||
|
||||
describe('without downstream pipeline', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ downstreamPipelinePath: undefined });
|
||||
});
|
||||
|
||||
it('does not render CTA button', () => {
|
||||
expect(findLinkBtn().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,99 +0,0 @@
|
|||
import { GlButton, GlDropdown } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import BridgeSidebar from '~/jobs/bridge/components/sidebar.vue';
|
||||
import CommitBlock from '~/jobs/components/commit_block.vue';
|
||||
import { mockCommit, mockJob } from '../mock_data';
|
||||
|
||||
describe('Bridge Sidebar', () => {
|
||||
let wrapper;
|
||||
|
||||
const MockHeaderEl = {
|
||||
getBoundingClientRect() {
|
||||
return {
|
||||
bottom: '40',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const createComponent = ({ featureFlag } = {}) => {
|
||||
wrapper = shallowMount(BridgeSidebar, {
|
||||
provide: {
|
||||
glFeatures: {
|
||||
triggerJobRetryAction: featureFlag,
|
||||
},
|
||||
},
|
||||
propsData: {
|
||||
bridgeJob: mockJob,
|
||||
commit: mockCommit,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findJobTitle = () => wrapper.find('h4');
|
||||
const findCommitBlock = () => wrapper.findComponent(CommitBlock);
|
||||
const findRetryDropdown = () => wrapper.find(GlDropdown);
|
||||
const findToggleBtn = () => wrapper.findComponent(GlButton);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders job name', () => {
|
||||
expect(findJobTitle().text()).toBe(mockJob.name);
|
||||
});
|
||||
|
||||
it('renders commit information', () => {
|
||||
expect(findCommitBlock().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('styles', () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(document, 'querySelector').mockReturnValue(MockHeaderEl);
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('calculates root styles correctly', () => {
|
||||
expect(wrapper.attributes('style')).toBe('width: 290px; top: 40px;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('sidebar expansion', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('emits toggle sidebar event on button click', async () => {
|
||||
expect(wrapper.emitted('toggleSidebar')).toBe(undefined);
|
||||
|
||||
findToggleBtn().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('toggleSidebar')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('retry action', () => {
|
||||
describe('when feature flag is ON', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ featureFlag: true });
|
||||
});
|
||||
|
||||
it('renders retry dropdown', () => {
|
||||
expect(findRetryDropdown().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when feature flag is OFF', () => {
|
||||
it('does not render retry dropdown', () => {
|
||||
createComponent({ featureFlag: false });
|
||||
|
||||
expect(findRetryDropdown().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,102 +0,0 @@
|
|||
export const MOCK_EMPTY_ILLUSTRATION_PATH = '/path/to/svg';
|
||||
export const MOCK_PATH_TO_DOWNSTREAM = '/path/to/downstream/pipeline';
|
||||
export const MOCK_BUILD_ID = '1331';
|
||||
export const MOCK_PIPELINE_IID = '174';
|
||||
export const MOCK_PROJECT_FULL_PATH = '/root/project/';
|
||||
export const MOCK_SHA = '38f3d89147765427a7ce58be28cd76d14efa682a';
|
||||
|
||||
export const mockCommit = {
|
||||
id: `gid://gitlab/CommitPresenter/${MOCK_SHA}`,
|
||||
shortId: '38f3d891',
|
||||
title: 'Update .gitlab-ci.yml file',
|
||||
webPath: `/root/project/-/commit/${MOCK_SHA}`,
|
||||
__typename: 'Commit',
|
||||
};
|
||||
|
||||
export const mockJob = {
|
||||
createdAt: '2021-12-10T09:05:45Z',
|
||||
id: 'gid://gitlab/Ci::Build/1331',
|
||||
name: 'triggerJobName',
|
||||
scheduledAt: null,
|
||||
startedAt: '2021-12-10T09:13:43Z',
|
||||
status: 'SUCCESS',
|
||||
triggered: null,
|
||||
detailedStatus: {
|
||||
id: '1',
|
||||
detailsPath: '/root/project/-/jobs/1331',
|
||||
icon: 'status_success',
|
||||
group: 'success',
|
||||
text: 'passed',
|
||||
tooltip: 'passed',
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
downstreamPipeline: {
|
||||
id: '1',
|
||||
path: '/root/project/-/pipelines/175',
|
||||
},
|
||||
stage: {
|
||||
id: '1',
|
||||
name: 'build',
|
||||
__typename: 'CiStage',
|
||||
},
|
||||
__typename: 'CiJob',
|
||||
};
|
||||
|
||||
export const mockUser = {
|
||||
id: 'gid://gitlab/User/1',
|
||||
avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
|
||||
name: 'Administrator',
|
||||
username: 'root',
|
||||
webPath: '/root',
|
||||
webUrl: 'http://gdk.test:3000/root',
|
||||
status: {
|
||||
message: 'making great things',
|
||||
__typename: 'UserStatus',
|
||||
},
|
||||
__typename: 'UserCore',
|
||||
};
|
||||
|
||||
export const mockStage = {
|
||||
id: '1',
|
||||
name: 'build',
|
||||
jobs: {
|
||||
nodes: [mockJob],
|
||||
__typename: 'CiJobConnection',
|
||||
},
|
||||
__typename: 'CiStage',
|
||||
};
|
||||
|
||||
export const mockPipelineQueryResponse = {
|
||||
data: {
|
||||
project: {
|
||||
id: '1',
|
||||
pipeline: {
|
||||
commit: mockCommit,
|
||||
id: 'gid://gitlab/Ci::Pipeline/174',
|
||||
iid: '88',
|
||||
path: '/root/project/-/pipelines/174',
|
||||
sha: MOCK_SHA,
|
||||
ref: 'main',
|
||||
refPath: 'path/to/ref',
|
||||
user: mockUser,
|
||||
detailedStatus: {
|
||||
id: '1',
|
||||
icon: 'status_failed',
|
||||
group: 'failed',
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
stages: {
|
||||
edges: [
|
||||
{
|
||||
node: mockStage,
|
||||
__typename: 'CiStageEdge',
|
||||
},
|
||||
],
|
||||
__typename: 'CiStageConnection',
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -14,6 +14,7 @@ jest.mock('~/lib/utils/url_utility');
|
|||
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} };
|
||||
const LeadTimeChartsStub = { name: 'LeadTimeCharts', render: () => {} };
|
||||
const TimeToRestoreServiceChartsStub = { name: 'TimeToRestoreServiceCharts', render: () => {} };
|
||||
const ChangeFailureRateChartsStub = { name: 'ChangeFailureRateCharts', render: () => {} };
|
||||
const ProjectQualitySummaryStub = { name: 'ProjectQualitySummary', render: () => {} };
|
||||
|
||||
describe('ProjectsPipelinesChartsApp', () => {
|
||||
|
@ -33,6 +34,7 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
|
||||
LeadTimeCharts: LeadTimeChartsStub,
|
||||
TimeToRestoreServiceCharts: TimeToRestoreServiceChartsStub,
|
||||
ChangeFailureRateCharts: ChangeFailureRateChartsStub,
|
||||
ProjectQualitySummary: ProjectQualitySummaryStub,
|
||||
},
|
||||
},
|
||||
|
@ -50,6 +52,7 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
const findGlTabAtIndex = (index) => findAllGlTabs().at(index);
|
||||
const findLeadTimeCharts = () => wrapper.find(LeadTimeChartsStub);
|
||||
const findTimeToRestoreServiceCharts = () => wrapper.find(TimeToRestoreServiceChartsStub);
|
||||
const findChangeFailureRateCharts = () => wrapper.find(ChangeFailureRateChartsStub);
|
||||
const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
|
||||
const findPipelineCharts = () => wrapper.find(PipelineCharts);
|
||||
const findProjectQualitySummary = () => wrapper.find(ProjectQualitySummaryStub);
|
||||
|
@ -59,58 +62,49 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
createComponent();
|
||||
});
|
||||
|
||||
it('renders tabs', () => {
|
||||
expect(findGlTabs().exists()).toBe(true);
|
||||
|
||||
expect(findGlTabAtIndex(0).attributes('title')).toBe('Pipelines');
|
||||
expect(findGlTabAtIndex(1).attributes('title')).toBe('Deployment frequency');
|
||||
expect(findGlTabAtIndex(2).attributes('title')).toBe('Lead time');
|
||||
expect(findGlTabAtIndex(3).attributes('title')).toBe('Time to restore service');
|
||||
});
|
||||
|
||||
it('renders the pipeline charts', () => {
|
||||
expect(findPipelineCharts().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the deployment frequency charts', () => {
|
||||
expect(findDeploymentFrequencyCharts().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the lead time charts', () => {
|
||||
expect(findLeadTimeCharts().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the time to restore service charts', () => {
|
||||
expect(findTimeToRestoreServiceCharts().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the project quality summary', () => {
|
||||
expect(findProjectQualitySummary().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('sets the tab and url when a tab is clicked', async () => {
|
||||
let chartsPath;
|
||||
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
|
||||
|
||||
mergeUrlParams.mockImplementation(({ chart }, path) => {
|
||||
expect(chart).toBe('deployment-frequency');
|
||||
expect(path).toBe(window.location.pathname);
|
||||
chartsPath = `${path}?chart=${chart}`;
|
||||
return chartsPath;
|
||||
describe.each`
|
||||
title | finderFn | index
|
||||
${'Pipelines'} | ${findPipelineCharts} | ${0}
|
||||
${'Deployment frequency'} | ${findDeploymentFrequencyCharts} | ${1}
|
||||
${'Lead time'} | ${findLeadTimeCharts} | ${2}
|
||||
${'Time to restore service'} | ${findTimeToRestoreServiceCharts} | ${3}
|
||||
${'Change failure rate'} | ${findChangeFailureRateCharts} | ${4}
|
||||
${'Project quality'} | ${findProjectQualitySummary} | ${5}
|
||||
`('Tabs', ({ title, finderFn, index }) => {
|
||||
it(`renders tab with a title ${title} at index ${index}`, () => {
|
||||
expect(findGlTabAtIndex(index).attributes('title')).toBe(title);
|
||||
});
|
||||
|
||||
updateHistory.mockImplementation(({ url }) => {
|
||||
expect(url).toBe(chartsPath);
|
||||
it(`renders the ${title} chart`, () => {
|
||||
expect(finderFn().exists()).toBe(true);
|
||||
});
|
||||
const tabs = findGlTabs();
|
||||
|
||||
expect(tabs.attributes('value')).toBe('0');
|
||||
it(`updates the current tab and url when the ${title} tab is clicked`, async () => {
|
||||
let chartsPath;
|
||||
const tabName = title.toLowerCase().replace(/\s/g, '-');
|
||||
|
||||
tabs.vm.$emit('input', 1);
|
||||
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
|
||||
|
||||
await nextTick();
|
||||
mergeUrlParams.mockImplementation(({ chart }, path) => {
|
||||
expect(chart).toBe(tabName);
|
||||
expect(path).toBe(window.location.pathname);
|
||||
chartsPath = `${path}?chart=${chart}`;
|
||||
return chartsPath;
|
||||
});
|
||||
|
||||
expect(tabs.attributes('value')).toBe('1');
|
||||
updateHistory.mockImplementation(({ url }) => {
|
||||
expect(url).toBe(chartsPath);
|
||||
});
|
||||
const tabs = findGlTabs();
|
||||
|
||||
expect(tabs.attributes('value')).toBe('0');
|
||||
|
||||
tabs.vm.$emit('input', index);
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(tabs.attributes('value')).toBe(index.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it('should not try to push history if the tab does not change', async () => {
|
||||
|
@ -151,6 +145,7 @@ describe('ProjectsPipelinesChartsApp', () => {
|
|||
describe('when provided with a query param', () => {
|
||||
it.each`
|
||||
chart | tab
|
||||
${'change-failure-rate'} | ${'4'}
|
||||
${'time-to-restore-service'} | ${'3'}
|
||||
${'lead-time'} | ${'2'}
|
||||
${'deployment-frequency'} | ${'1'}
|
||||
|
|
|
@ -77,7 +77,7 @@ RSpec.describe EmailsHelper do
|
|||
end
|
||||
|
||||
describe 'notification_reason_text' do
|
||||
subject { helper.notification_reason_text(reason_code) }
|
||||
subject { helper.notification_reason_text(reason: reason_code) }
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
|
|
@ -7,13 +7,12 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do
|
|||
|
||||
let(:builder) { described_class.new(merge_request) }
|
||||
|
||||
describe '#build' do
|
||||
let(:data) { builder.build }
|
||||
describe '.safe_hook_attributes' do
|
||||
let(:safe_attribute_keys) { described_class.safe_hook_attributes }
|
||||
|
||||
it 'includes safe attribute' do
|
||||
%w[
|
||||
expected_safe_attribute_keys = %i[
|
||||
assignee_id
|
||||
assignee_ids
|
||||
author_id
|
||||
blocking_discussions_resolved
|
||||
created_at
|
||||
|
@ -32,17 +31,21 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do
|
|||
milestone_id
|
||||
source_branch
|
||||
source_project_id
|
||||
state
|
||||
state_id
|
||||
target_branch
|
||||
target_project_id
|
||||
time_estimate
|
||||
title
|
||||
updated_at
|
||||
updated_by_id
|
||||
].each do |key|
|
||||
expect(data).to include(key)
|
||||
end
|
||||
].freeze
|
||||
|
||||
expect(safe_attribute_keys).to match_array(expected_safe_attribute_keys)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#build' do
|
||||
let(:data) { builder.build }
|
||||
|
||||
%i[source target].each do |key|
|
||||
describe "#{key} key" do
|
||||
|
@ -52,17 +55,30 @@ RSpec.describe Gitlab::HookData::MergeRequestBuilder do
|
|||
end
|
||||
end
|
||||
|
||||
it 'includes safe attributes' do
|
||||
expect(data).to include(*described_class.safe_hook_attributes)
|
||||
end
|
||||
|
||||
it 'includes additional attrs' do
|
||||
expect(data).to include(:source)
|
||||
expect(data).to include(:target)
|
||||
expect(data).to include(:last_commit)
|
||||
expect(data).to include(:work_in_progress)
|
||||
expect(data).to include(:total_time_spent)
|
||||
expect(data).to include(:time_change)
|
||||
expect(data).to include(:human_time_estimate)
|
||||
expect(data).to include(:human_total_time_spent)
|
||||
expect(data).to include(:human_time_change)
|
||||
expect(data).to include(:labels)
|
||||
expected_additional_attributes = %w[
|
||||
description
|
||||
url
|
||||
last_commit
|
||||
work_in_progress
|
||||
total_time_spent
|
||||
time_change
|
||||
human_total_time_spent
|
||||
human_time_change
|
||||
human_time_estimate
|
||||
assignee_ids
|
||||
assignee_id
|
||||
labels
|
||||
state
|
||||
blocking_discussions_resolved
|
||||
first_contribution
|
||||
].freeze
|
||||
|
||||
expect(data).to include(*expected_additional_attributes)
|
||||
end
|
||||
|
||||
context 'when the MR has an image in the description' do
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Metrics::Memory do
|
||||
describe '.gc_heap_fragmentation' do
|
||||
subject(:call) do
|
||||
described_class.gc_heap_fragmentation(
|
||||
heap_live_slots: gc_stat_heap_live_slots,
|
||||
heap_eden_pages: gc_stat_heap_eden_pages
|
||||
)
|
||||
end
|
||||
|
||||
context 'when the Ruby heap is perfectly utilized' do
|
||||
# All objects are located in a single heap page.
|
||||
let(:gc_stat_heap_live_slots) { described_class::HEAP_SLOTS_PER_PAGE }
|
||||
let(:gc_stat_heap_eden_pages) { 1 }
|
||||
|
||||
it { is_expected.to eq(0) }
|
||||
end
|
||||
|
||||
context 'when the Ruby heap is greatly fragmented' do
|
||||
# There is one object per heap page.
|
||||
let(:gc_stat_heap_live_slots) { described_class::HEAP_SLOTS_PER_PAGE }
|
||||
let(:gc_stat_heap_eden_pages) { described_class::HEAP_SLOTS_PER_PAGE }
|
||||
|
||||
# The heap can never be "perfectly fragmented" because that would require
|
||||
# zero objects per page.
|
||||
it { is_expected.to be > 0.99 }
|
||||
end
|
||||
|
||||
context 'when the Ruby heap is semi-fragmented' do
|
||||
# All objects are spread over two pages i.e. each page is 50% utilized.
|
||||
let(:gc_stat_heap_live_slots) { described_class::HEAP_SLOTS_PER_PAGE }
|
||||
let(:gc_stat_heap_eden_pages) { 2 }
|
||||
|
||||
it { is_expected.to eq(0.5) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -125,5 +125,11 @@ RSpec.describe Gitlab::Metrics::Samplers::RubySampler do
|
|||
|
||||
sampler.sample
|
||||
end
|
||||
|
||||
it 'adds a heap fragmentation metric' do
|
||||
expect(sampler.metrics[:heap_fragmentation]).to receive(:set).with({}, anything)
|
||||
|
||||
sampler.sample
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -138,5 +138,20 @@ RSpec.describe UnnestedInFilters::Rewriter do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'logging' do
|
||||
subject(:load_reload) { rewriter.rewrite }
|
||||
|
||||
before do
|
||||
allow(::Gitlab::AppLogger).to receive(:info)
|
||||
end
|
||||
|
||||
it 'logs the call' do
|
||||
load_reload
|
||||
|
||||
expect(::Gitlab::AppLogger)
|
||||
.to have_received(:info).with(message: 'Query is being rewritten by `UnnestedInFilters`', model: 'User')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -64,7 +64,7 @@ RSpec.describe Emails::AdminNotification do
|
|||
end
|
||||
|
||||
it 'includes the email reason' do
|
||||
is_expected.to have_body_text "You're receiving this email because of your account on localhost"
|
||||
is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>}
|
||||
end
|
||||
|
||||
context 'when scoped to a group' do
|
||||
|
|
|
@ -151,7 +151,7 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
|
||||
it 'includes the email reason' do
|
||||
is_expected.to have_body_text /You're receiving this email because of your account on localhost/
|
||||
is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -187,7 +187,7 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
|
||||
it 'includes the email reason' do
|
||||
is_expected.to have_body_text /You're receiving this email because of your account on localhost/
|
||||
is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>}
|
||||
end
|
||||
|
||||
context 'with User does not exist' do
|
||||
|
@ -222,7 +222,7 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
|
||||
it 'includes the email reason' do
|
||||
is_expected.to have_body_text /You're receiving this email because of your account on localhost/
|
||||
is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>}
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -266,7 +266,7 @@ RSpec.describe Emails::Profile do
|
|||
end
|
||||
|
||||
shared_examples 'includes the email reason' do
|
||||
it { is_expected.to have_body_text /You're receiving this email because of your account on localhost/ }
|
||||
it { is_expected.to have_body_text %r{You're receiving this email because of your account on <a .*>localhost<\/a>} }
|
||||
end
|
||||
|
||||
shared_examples 'valid use case' do
|
||||
|
|
|
@ -7,6 +7,7 @@ RSpec.describe Notify do
|
|||
include EmailSpec::Helpers
|
||||
include EmailSpec::Matchers
|
||||
include EmailHelpers
|
||||
include EmailsHelper
|
||||
include RepoHelpers
|
||||
include MembersHelper
|
||||
|
||||
|
@ -396,7 +397,7 @@ RSpec.describe Notify do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when sent with a reason' do
|
||||
context 'when sent with a reason', type: :helper do
|
||||
subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) }
|
||||
|
||||
it_behaves_like 'appearance header and footer enabled'
|
||||
|
@ -407,15 +408,15 @@ RSpec.describe Notify do
|
|||
end
|
||||
|
||||
it 'includes the reason in the footer' do
|
||||
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::ASSIGNED)
|
||||
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(reason: NotificationReason::ASSIGNED, format: :html)
|
||||
is_expected.to have_body_text(text)
|
||||
|
||||
new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::MENTIONED)
|
||||
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::MENTIONED)
|
||||
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(reason: NotificationReason::MENTIONED, format: :html)
|
||||
expect(new_subject).to have_body_text(text)
|
||||
|
||||
new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, nil)
|
||||
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(nil)
|
||||
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(format: :html)
|
||||
expect(new_subject).to have_body_text(text)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -97,10 +97,8 @@ RSpec.describe MergeRequestDiffFile do
|
|||
|
||||
file.utf8_diff
|
||||
end
|
||||
end
|
||||
|
||||
describe '#diff_export' do
|
||||
context 'when diff is externally stored' do
|
||||
context 'externally stored diff caching' do
|
||||
let(:test_dir) { 'tmp/tests/external-diffs' }
|
||||
|
||||
around do |example|
|
||||
|
@ -121,7 +119,7 @@ RSpec.describe MergeRequestDiffFile do
|
|||
it 'caches external diffs' do
|
||||
expect(file.merge_request_diff).to receive(:cache_external_diff).and_call_original
|
||||
|
||||
expect(file.diff_export).to eq(file.utf8_diff)
|
||||
expect(file.utf8_diff).to eq(file.diff)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -133,7 +131,7 @@ RSpec.describe MergeRequestDiffFile do
|
|||
expect(file_stub).to receive(:seek).with(file.external_diff_offset)
|
||||
expect(file_stub).to receive(:read).with(file.external_diff_size)
|
||||
|
||||
file.diff_export
|
||||
file.utf8_diff
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -151,28 +149,28 @@ RSpec.describe MergeRequestDiffFile do
|
|||
end
|
||||
|
||||
it 'unpacks from base 64' do
|
||||
expect(file.diff_export).to eq(unpacked)
|
||||
expect(file.utf8_diff).to eq(unpacked)
|
||||
end
|
||||
|
||||
context 'invalid base64' do
|
||||
let(:packed) { '---/dev/null' }
|
||||
|
||||
it 'returns the raw diff' do
|
||||
expect(file.diff_export).to eq(packed)
|
||||
expect(file.utf8_diff).to eq(packed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the diff is not marked as binary' do
|
||||
it 'returns the raw diff' do
|
||||
expect(file.diff_export).to eq(packed)
|
||||
expect(file.utf8_diff).to eq(packed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when content responds to #encoding' do
|
||||
it 'encodes content to utf8 encoding' do
|
||||
expect(file.diff_export.encoding).to eq(Encoding::UTF_8)
|
||||
expect(file.utf8_diff.encoding).to eq(Encoding::UTF_8)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -180,35 +178,46 @@ RSpec.describe MergeRequestDiffFile do
|
|||
it 'returns an empty string' do
|
||||
allow(file.merge_request_diff).to receive(:cached_external_diff).and_return(nil)
|
||||
|
||||
expect(file.diff_export).to eq('')
|
||||
expect(file.utf8_diff).to eq('')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when exception is raised' do
|
||||
it 'falls back to #utf8_diff' do
|
||||
allow(file).to receive(:binary?).and_raise(StandardError)
|
||||
expect(file).to receive(:utf8_diff)
|
||||
it 'falls back to #diff' do
|
||||
allow(file).to receive(:binary?).and_raise(StandardError, 'Error!')
|
||||
expect(file).to receive(:diff)
|
||||
expect(Gitlab::AppLogger)
|
||||
.to receive(:warn)
|
||||
.with(
|
||||
a_hash_including(
|
||||
:message => 'Cached external diff export failed',
|
||||
:merge_request_diff_file_id => file.id,
|
||||
:merge_request_diff_id => file.merge_request_diff.id,
|
||||
'exception.class' => 'StandardError',
|
||||
'exception.message' => 'Error!'
|
||||
)
|
||||
)
|
||||
|
||||
file.diff_export
|
||||
file.utf8_diff
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when externally_stored_diffs_caching_export feature flag is disabled' do
|
||||
it 'calls #utf8_diff' do
|
||||
it 'calls #diff' do
|
||||
stub_feature_flags(externally_stored_diffs_caching_export: false)
|
||||
|
||||
expect(file).to receive(:utf8_diff)
|
||||
expect(file).to receive(:diff)
|
||||
|
||||
file.diff_export
|
||||
file.utf8_diff
|
||||
end
|
||||
end
|
||||
|
||||
context 'when diff is not stored externally' do
|
||||
it 'calls #utf8_diff' do
|
||||
expect(file).to receive(:utf8_diff)
|
||||
it 'calls #diff' do
|
||||
expect(file).to receive(:diff)
|
||||
|
||||
file.diff_export
|
||||
file.utf8_diff
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -171,6 +171,75 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version' do
|
||||
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}") }
|
||||
let(:headers) { {} }
|
||||
|
||||
subject { get(url, headers: headers) }
|
||||
|
||||
context 'not found' do
|
||||
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/2.0.0") }
|
||||
let(:headers) { { 'Authorization' => "Bearer #{tokens[:job_token]}" } }
|
||||
|
||||
subject { get(url, headers: headers) }
|
||||
|
||||
it 'returns not found when the specified version is not present in the registry' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid namespace' do
|
||||
where(:visibility, :user_role, :member, :token_type, :shared_examples_name, :expected_status) do
|
||||
:public | :developer | true | :personal_access_token | 'returns terraform module version' | :success
|
||||
:public | :guest | true | :personal_access_token | 'returns terraform module version' | :success
|
||||
:public | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | :personal_access_token | 'returns terraform module version' | :success
|
||||
:public | :guest | false | :personal_access_token | 'returns terraform module version' | :success
|
||||
:public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :anonymous | false | nil | 'returns terraform module version' | :success
|
||||
:private | :developer | true | :personal_access_token | 'returns terraform module version' | :success
|
||||
:private | :guest | true | :personal_access_token | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :guest | false | :personal_access_token | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :anonymous | false | nil | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | true | :job_token | 'returns terraform module version' | :success
|
||||
:public | :guest | true | :job_token | 'returns terraform module version' | :success
|
||||
:public | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :developer | false | :job_token | 'returns terraform module version' | :success
|
||||
:public | :guest | false | :job_token | 'returns terraform module version' | :success
|
||||
:public | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:public | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | true | :job_token | 'returns terraform module version' | :success
|
||||
:private | :guest | true | :job_token | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | true | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | true | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :developer | false | :job_token | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :guest | false | :job_token | 'rejects terraform module packages access' | :forbidden
|
||||
:private | :developer | false | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
:private | :guest | false | :invalid | 'rejects terraform module packages access' | :unauthorized
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:headers) { user_role == :anonymous ? {} : { 'Authorization' => "Bearer #{token}" } }
|
||||
|
||||
before do
|
||||
group.update!(visibility: visibility.to_s)
|
||||
project.update!(visibility: visibility.to_s)
|
||||
end
|
||||
|
||||
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/packages/terraform/modules/v1/:module_namespace/:module_name/:module_system/:module_version/download' do
|
||||
let(:url) { api("/packages/terraform/modules/v1/#{group.path}/#{package.name}/#{package.version}/download") }
|
||||
let(:headers) { {} }
|
||||
|
|
|
@ -269,6 +269,17 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context
|
|||
it 'returns items not assigned to that milestone' do
|
||||
expect(items).to contain_exactly(item2, item3, item4, item5)
|
||||
end
|
||||
|
||||
context 'with multiple milestones' do
|
||||
let(:milestone2) { create(:milestone, project: project2) }
|
||||
let(:params) { { not: { milestone_title: [milestone.title, milestone2.title] } } }
|
||||
|
||||
it 'returns items not assigned to both milestones' do
|
||||
item2.update!(milestone: milestone2)
|
||||
|
||||
expect(items).to contain_exactly(item3, item4, item5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by group milestone' do
|
||||
|
|
|
@ -102,6 +102,22 @@ RSpec.shared_examples 'returns terraform module packages' do |user_type, status,
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'returns terraform module version' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
group.send("add_#{user_type}", user) if add_member && user_type != :anonymous
|
||||
end
|
||||
|
||||
it_behaves_like 'returning response status', status
|
||||
|
||||
it 'returning a valid response' do
|
||||
subject
|
||||
|
||||
expect(json_response).to match_schema('public_api/v4/packages/terraform/modules/v1/single_version')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'returns no terraform module packages' do |user_type, status, add_member = true|
|
||||
context "for user type #{user_type}" do
|
||||
before do
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'projects/commits/_commit.html.haml' do
|
||||
let(:template) { 'projects/commits/commit.html.haml' }
|
||||
let(:template) { 'projects/commits/commit' }
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:commit) { project.repository.commit(ref) }
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ RSpec.describe 'projects/jobs/show' do
|
|||
|
||||
it 'shows job vue app' do
|
||||
expect(rendered).to have_css('#js-job-page')
|
||||
expect(rendered).not_to have_css('#js-bridge-page')
|
||||
end
|
||||
|
||||
context 'when job is running' do
|
||||
|
@ -42,18 +41,4 @@ RSpec.describe 'projects/jobs/show' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when showing a bridge job' do
|
||||
let(:bridge) { create(:ci_bridge, status: :pending) }
|
||||
|
||||
before do
|
||||
assign(:build, bridge)
|
||||
render
|
||||
end
|
||||
|
||||
it 'shows bridge vue app' do
|
||||
expect(rendered).to have_css('#js-bridge-page')
|
||||
expect(rendered).not_to have_css('#js-job-page')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,7 +25,7 @@ require (
|
|||
github.com/sebest/xff v0.0.0-20210106013422-671bd2870b3a
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/smartystreets/goconvey v1.7.2
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
gitlab.com/gitlab-org/gitaly/v15 v15.1.0
|
||||
gitlab.com/gitlab-org/golang-archive-zip v0.1.1
|
||||
gitlab.com/gitlab-org/labkit v1.15.0
|
||||
|
@ -113,5 +113,5 @@ require (
|
|||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220401170504-314d38edb7de // indirect
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.32.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
|
|
@ -1044,16 +1044,19 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3
|
|||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/tinylib/msgp v1.1.2 h1:gWmO7n0Ys2RBEb7GPYB9Ujq8Mk5p2U08lRnmMcGy6BQ=
|
||||
|
@ -1815,8 +1818,9 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
|||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
|
Loading…
Reference in New Issue