Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-16 15:09:38 +00:00
parent 9d67bc14cb
commit 2d8454515e
105 changed files with 1359 additions and 315 deletions

View File

@ -20,6 +20,8 @@ default:
- gitlab-org
# All jobs are interruptible by default
interruptible: true
# Default job timeout set to 90m https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10520
timeout: 90m
workflow:
rules:

View File

@ -172,6 +172,7 @@ dependency_scanning:
# # - 'export DAST_AUTH_URL="${DAST_WEBSITE}/users/sign_in"'
# # - 'export DAST_PASSWORD="${REVIEW_APPS_ROOT_PASSWORD}"'
# - /analyze -t $DAST_WEBSITE
# timeout: 4h
# artifacts:
# paths:
# - gl-dast-report.json # GitLab-specific

View File

@ -9,6 +9,7 @@ cache gems:
stage: test
needs: ["setup-test-env"]
variables:
BUNDLE_INSTALL_FLAGS: --with=production --with=development --with=test --jobs=2 --path=vendor --retry=3 --quiet
SETUP_DB: "false"
script:
- bundle package --all --all-platforms

View File

@ -1 +1 @@
8.36.0
8.37.0

View File

@ -70,8 +70,19 @@ export const fetchImport = ({ state, commit }, { newName, targetNamespace, repo
repoId: repo.id,
}),
)
.catch(() => {
createFlash(s__('ImportProjects|Importing the project failed'));
.catch(e => {
const serverErrorMessage = e?.response?.data?.errors;
const flashMessage = serverErrorMessage
? sprintf(
s__('ImportProjects|Importing the project failed: %{reason}'),
{
reason: serverErrorMessage,
},
false,
)
: s__('ImportProjects|Importing the project failed');
createFlash(flashMessage);
commit(types.RECEIVE_IMPORT_ERROR, repo.id);
});

View File

@ -9,15 +9,11 @@ import {
GlNewDropdown,
GlNewDropdownItem,
} from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import {
I18N_ALERT_SETTINGS_FORM,
NO_ISSUE_TEMPLATE_SELECTED,
TAKING_INCIDENT_ACTION_DOCS_LINK,
ISSUE_TEMPLATES_DOCS_LINK,
ERROR_MSG,
} from '../constants';
export default {
@ -31,7 +27,7 @@ export default {
GlNewDropdown,
GlNewDropdownItem,
},
inject: ['alertSettings', 'operationsSettingsEndpoint'],
inject: ['service', 'alertSettings'],
data() {
return {
templates: [NO_ISSUE_TEMPLATE_SELECTED, ...this.alertSettings.templates],
@ -65,23 +61,10 @@ export default {
},
updateAlertsIntegrationSettings() {
this.loading = true;
return axios
.patch(this.operationsSettingsEndpoint, {
project: {
incident_management_setting_attributes: this.formData,
},
})
.then(() => {
refreshCurrentPage();
})
.catch(({ response }) => {
const message = response?.data?.message || '';
createFlash(`${ERROR_MSG} ${message}`, 'alert');
})
.finally(() => {
this.loading = false;
});
this.service.updateSettings(this.formData).catch(() => {
this.loading = false;
});
},
},
};

View File

@ -1,6 +1,8 @@
<script>
import { GlButton, GlTabs, GlTab } from '@gitlab/ui';
import AlertsSettingsForm from './alerts_form.vue';
import PagerDutySettingsForm from './pagerduty_form.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { INTEGRATION_TABS_CONFIG, I18N_INTEGRATION_TABS } from '../constants';
export default {
@ -9,9 +11,19 @@ export default {
GlTabs,
GlTab,
AlertsSettingsForm,
PagerDutySettingsForm,
},
mixins: [glFeatureFlagMixin()],
tabs: INTEGRATION_TABS_CONFIG,
i18n: I18N_INTEGRATION_TABS,
methods: {
isFeatureFlagEnabled(tab) {
if (tab.featureFlag) {
return this.glFeatures[tab.featureFlag];
}
return true;
},
},
};
</script>
@ -37,7 +49,7 @@ export default {
<gl-tabs>
<gl-tab
v-for="(tab, index) in $options.tabs"
v-if="tab.active"
v-if="tab.active && isFeatureFlagEnabled(tab)"
:key="`${tab.title}_${index}`"
:title="tab.title"
>

View File

@ -0,0 +1,183 @@
<script>
import {
GlAlert,
GlButton,
GlSprintf,
GlLink,
GlIcon,
GlFormGroup,
GlFormInputGroup,
GlToggle,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { I18N_PAGERDUTY_SETTINGS_FORM, CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK } from '../constants';
import { isEqual } from 'lodash';
export default {
components: {
GlAlert,
GlButton,
GlSprintf,
GlLink,
GlIcon,
GlFormGroup,
GlFormInputGroup,
GlToggle,
GlModal,
ClipboardButton,
},
directives: {
'gl-modal': GlModalDirective,
},
inject: ['service', 'pagerDutySettings'],
data() {
return {
active: this.pagerDutySettings.active,
webhookUrl: this.pagerDutySettings.webhookUrl,
loading: false,
resettingWebhook: false,
webhookUpdateFailed: false,
showAlert: false,
};
},
i18n: I18N_PAGERDUTY_SETTINGS_FORM,
CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK,
computed: {
formData() {
return {
pagerduty_active: this.active,
};
},
isFormUpdated() {
return isEqual(this.pagerDutySettings, {
active: this.active,
webhookUrl: this.webhookUrl,
});
},
isSaveDisabled() {
return this.isFormUpdated || this.loading || this.resettingWebhook;
},
webhookUpdateAlertMsg() {
return this.webhookUpdateFailed
? this.$options.i18n.webhookUrl.updateErrMsg
: this.$options.i18n.webhookUrl.updateSuccessMsg;
},
webhookUpdateAlertVariant() {
return this.webhookUpdateFailed ? 'danger' : 'success';
},
},
methods: {
updatePagerDutyIntegrationSettings() {
this.loading = true;
this.service.updateSettings(this.formData).catch(() => {
this.loading = false;
});
},
resetWebhookUrl() {
this.resettingWebhook = true;
this.service
.resetWebhookUrl()
.then(({ data: { pagerduty_webhook_url: url } }) => {
this.webhookUrl = url;
this.showAlert = true;
this.webhookUpdateFailed = false;
})
.catch(() => {
this.showAlert = true;
this.webhookUpdateFailed = true;
})
.finally(() => {
this.resettingWebhook = false;
});
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="showAlert"
class="gl-mb-3"
:variant="webhookUpdateAlertVariant"
@dismiss="showAlert = false"
>
{{ webhookUpdateAlertMsg }}
</gl-alert>
<p>{{ $options.i18n.introText }}</p>
<form ref="settingsForm" @submit.prevent="updatePagerDutyIntegrationSettings">
<gl-form-group class="col-8 col-md-9 gl-p-0">
<gl-toggle
id="active"
v-model="active"
:is-loading="loading"
:label="$options.i18n.activeToggle.label"
/>
</gl-form-group>
<gl-form-group
class="col-8 col-md-9 gl-p-0"
:label="$options.i18n.webhookUrl.label"
label-for="url"
label-class="label-bold"
>
<gl-form-input-group id="url" data-testid="webhook-url" readonly :value="webhookUrl">
<template #append>
<clipboard-button
:text="webhookUrl"
:title="$options.i18n.webhookUrl.copyToClipboard"
/>
</template>
</gl-form-input-group>
<div class="gl-text-gray-400 gl-pt-2">
<gl-sprintf :message="$options.i18n.webhookUrl.helpText">
<template #docsLink>
<gl-link
:href="$options.CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK"
target="_blank"
class="gl-display-inline-flex"
>
<span>{{ $options.i18n.webhookUrl.helpDocsLink }}</span>
<gl-icon name="external-link" />
</gl-link>
</template>
</gl-sprintf>
</div>
<gl-button
v-gl-modal.resetWebhookModal
class="gl-mt-3"
:disabled="loading"
:loading="resettingWebhook"
data-testid="webhook-reset-btn"
>
{{ $options.i18n.webhookUrl.resetWebhookUrl }}
</gl-button>
<gl-modal
modal-id="resetWebhookModal"
:title="$options.i18n.webhookUrl.resetWebhookUrl"
:ok-title="$options.i18n.webhookUrl.resetWebhookUrl"
ok-variant="danger"
@ok="resetWebhookUrl"
>
{{ $options.i18n.webhookUrl.restKeyInfo }}
</gl-modal>
</gl-form-group>
<gl-button
ref="submitBtn"
:disabled="isSaveDisabled"
variant="success"
type="submit"
class="js-no-auto-disable"
>
{{ $options.i18n.saveBtnLabel }}
</gl-button>
</form>
</div>
</template>

View File

@ -1,5 +1,6 @@
import { __, s__ } from '~/locale';
/* Integration tabs constants */
export const INTEGRATION_TABS_CONFIG = [
{
title: s__('IncidentSettings|Alert integration'),
@ -8,8 +9,9 @@ export const INTEGRATION_TABS_CONFIG = [
},
{
title: s__('IncidentSettings|PagerDuty integration'),
component: '',
active: false,
component: 'PagerDutySettingsForm',
active: true,
featureFlag: 'pagerdutyWebhook',
},
{
title: s__('IncidentSettings|Grafana integration'),
@ -21,12 +23,13 @@ export const INTEGRATION_TABS_CONFIG = [
export const I18N_INTEGRATION_TABS = {
headerText: s__('IncidentSettings|Incidents'),
expandBtnLabel: __('Expand'),
saveBtnLabel: __('Save changes'),
subHeaderText: s__(
'IncidentSettings|Set up integrations with external tools to help better manage incidents.',
),
};
/* Alerts integration settings constants */
export const I18N_ALERT_SETTINGS_FORM = {
saveBtnLabel: __('Save changes'),
introText: __('Action to take when receiving an alert. %{docsLink}'),
@ -48,4 +51,33 @@ export const TAKING_INCIDENT_ACTION_DOCS_LINK =
export const ISSUE_TEMPLATES_DOCS_LINK =
'/help/user/project/description_templates#creating-issue-templates';
/* PagerDuty integration settings constants */
export const I18N_PAGERDUTY_SETTINGS_FORM = {
introText: s__(
'PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.',
),
activeToggle: {
label: s__('PagerDutySettings|Active'),
},
webhookUrl: {
label: s__('PagerDutySettings|Webhook URL'),
helpText: s__(
'PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}',
),
helpDocsLink: s__('PagerDutySettings|configuring a webhook in PagerDuty'),
resetWebhookUrl: s__('PagerDutySettings|Reset webhook URL'),
copyToClipboard: __('Copy'),
updateErrMsg: s__('PagerDutySettings|Failed to update Webhook URL'),
updateSuccessMsg: s__('PagerDutySettings|Webhook URL update was successful'),
restKeyInfo: s__(
"PagerDutySettings|Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty.",
),
},
saveBtnLabel: __('Save changes'),
};
export const CONFIGURE_PAGERDUTY_WEBHOOK_DOCS_LINK = 'https://support.pagerduty.com/docs/webhooks';
/* common constants */
export const ERROR_MSG = __('There was an error saving your changes.');

View File

@ -0,0 +1,32 @@
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import createFlash from '~/flash';
import { ERROR_MSG } from './constants';
export default class IncidentsSettingsService {
constructor(settingsEndpoint, webhookUpdateEndpoint) {
this.settingsEndpoint = settingsEndpoint;
this.webhookUpdateEndpoint = webhookUpdateEndpoint;
}
updateSettings(data) {
return axios
.patch(this.settingsEndpoint, {
project: {
incident_management_setting_attributes: data,
},
})
.then(() => {
refreshCurrentPage();
})
.catch(({ response }) => {
const message = response?.data?.message || '';
createFlash(`${ERROR_MSG} ${message}`, 'alert');
});
}
resetWebhookUrl() {
return axios.post(this.webhookUpdateEndpoint);
}
}

View File

@ -1,6 +1,7 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import SettingsTabs from './components/incidents_settings_tabs.vue';
import IncidentsSettingsService from './incidents_settings_service';
export default () => {
const el = document.querySelector('.js-incidents-settings');
@ -10,19 +11,33 @@ export default () => {
}
const {
dataset: { operationsSettingsEndpoint, templates, createIssue, issueTemplateKey, sendEmail },
dataset: {
operationsSettingsEndpoint,
templates,
createIssue,
issueTemplateKey,
sendEmail,
pagerdutyActive,
pagerdutyWebhookUrl,
pagerdutyResetKeyPath,
},
} = el;
const service = new IncidentsSettingsService(operationsSettingsEndpoint, pagerdutyResetKeyPath);
return new Vue({
el,
provide: {
operationsSettingsEndpoint,
service,
alertSettings: {
templates: JSON.parse(templates),
createIssue: parseBoolean(createIssue),
issueTemplateKey,
sendEmail: parseBoolean(sendEmail),
},
pagerDutySettings: {
active: parseBoolean(pagerdutyActive),
webhookUrl: pagerdutyWebhookUrl,
},
},
render(createElement) {
return createElement(SettingsTabs);

View File

@ -29,9 +29,9 @@ export default {
return [
'gl-display-flex',
'gl-flex-direction-column',
'gl-absolute',
'gl-fixed',
'gl-right-1',
'gl-top-0',
'gl-top-66vh',
'gl-w-max-content',
'gl-px-5',
'gl-py-4',

View File

@ -43,6 +43,7 @@ export default {
data() {
return {
downstreamMarginTop: null,
jobName: null,
};
},
computed: {
@ -91,13 +92,9 @@ export default {
/**
* Calculates the margin top of the clicked downstream pipeline by
* subtracting the clicked downstream pipelines offsetTop by it's parent's
* offsetTop and then subtracting either 15 (if child) or 30 (if not a child)
* due to the height of node and stage name margin bottom.
* offsetTop and then subtracting 15
*/
this.downstreamMarginTop = this.calculateMarginTop(
downstreamNode,
downstreamNode.classList.contains('child-pipeline') ? 15 : 30,
);
this.downstreamMarginTop = this.calculateMarginTop(downstreamNode, 15);
/**
* If the expanded trigger is defined and the id is different than the
@ -120,6 +117,9 @@ export default {
hasUpstream(index) {
return index === 0 && this.hasTriggeredBy;
},
setJob(jobName) {
this.jobName = jobName;
},
},
};
</script>
@ -180,6 +180,7 @@ export default {
:is-first-column="isFirstColumn(index)"
:has-triggered-by="hasTriggeredBy"
:action="stage.status.action"
:job-hovered="jobName"
@refreshPipelineGraph="refreshPipelineGraph"
/>
</ul>
@ -191,6 +192,7 @@ export default {
:project-id="pipelineProjectId"
graph-position="right"
@linkedPipelineClick="handleClickedDownstream"
@downstreamHovered="setJob"
/>
<pipeline-graph

View File

@ -31,6 +31,7 @@ import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
*/
export default {
hoverClass: 'gl-inset-border-1-blue-500',
components: {
ActionComponent,
JobNameComponent,
@ -55,6 +56,11 @@ export default {
required: false,
default: Infinity,
},
jobHovered: {
type: String,
required: false,
default: '',
},
},
computed: {
boundary() {
@ -95,6 +101,11 @@ export default {
hasAction() {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
jobClasses() {
return this.job.name === this.jobHovered
? `${this.$options.hoverClass} ${this.cssClassJobName}`
: this.cssClassJobName;
},
},
methods: {
pipelineActionRequestComplete() {
@ -120,8 +131,9 @@ export default {
v-else
v-gl-tooltip="{ boundary, placement: 'bottom' }"
:title="tooltipText"
:class="cssClassJobName"
:class="jobClasses"
class="js-job-component-tooltip non-details-job-component"
data-testid="job-without-link"
>
<job-name-component :name="job.name" :status="job.status" />
</div>

View File

@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlTooltipDirective, GlDeprecatedButton } from '@gitlab/ui';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
export default {
directives: {
@ -28,7 +28,8 @@ export default {
},
computed: {
tooltipText() {
return `${this.projectName} - ${this.pipelineStatus.label}`;
return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label}
${this.sourceJobInfo}`;
},
buttonId() {
return `js-linked-pipeline-${this.pipeline.id}`;
@ -39,25 +40,32 @@ export default {
projectName() {
return this.pipeline.project.name;
},
downstreamTitle() {
return this.childPipeline ? __('child-pipeline') : this.pipeline.project.name;
},
parentPipeline() {
// Refactor string match when BE returns Upstream/Downstream indicators
return this.projectId === this.pipeline.project.id && this.columnTitle === __('Upstream');
},
childPipeline() {
// Refactor string match when BE returns Upstream/Downstream indicators
return this.projectId === this.pipeline.project.id && this.columnTitle === __('Downstream');
return this.projectId === this.pipeline.project.id && this.isDownstream;
},
label() {
return this.parentPipeline ? __('Parent') : __('Child');
if (this.parentPipeline) {
return __('Parent');
} else if (this.childPipeline) {
return __('Child');
}
return __('Multi-project');
},
childTooltipText() {
return __('This pipeline was triggered by a parent pipeline');
isDownstream() {
return this.columnTitle === __('Downstream');
},
parentTooltipText() {
return __('This pipeline triggered a child pipeline');
},
labelToolTipText() {
return this.label === __('Parent') ? this.parentTooltipText : this.childTooltipText;
sourceJobInfo() {
return this.isDownstream
? sprintf(__('Created by %{job}'), { job: this.pipeline.source_job.name })
: '';
},
},
methods: {
@ -68,6 +76,12 @@ export default {
hideTooltips() {
this.$root.$emit('bv::hide::tooltip');
},
onDownstreamHovered() {
this.$emit('downstreamHovered', this.pipeline.source_job.name);
},
onDownstreamHoverLeave() {
this.$emit('downstreamHovered', '');
},
},
};
</script>
@ -76,8 +90,10 @@ export default {
<li
ref="linkedPipeline"
class="linked-pipeline build"
:class="{ 'child-pipeline': childPipeline }"
:class="{ 'downstream-pipeline': isDownstream }"
data-qa-selector="child_pipeline"
@mouseover="onDownstreamHovered"
@mouseleave="onDownstreamHoverLeave"
>
<gl-deprecated-button
:id="buttonId"
@ -95,15 +111,9 @@ export default {
css-classes="position-top-0"
class="js-linked-pipeline-status"
/>
<span class="str-truncated align-bottom"> {{ projectName }} &#8226; #{{ pipeline.id }} </span>
<div v-if="parentPipeline || childPipeline" class="parent-child-label-container">
<span
v-gl-tooltip.bottom
:title="labelToolTipText"
class="badge badge-primary"
@mouseover="hideTooltips"
>{{ label }}</span
>
<span class="str-truncated"> {{ downstreamTitle }} &#8226; #{{ pipeline.id }} </span>
<div class="gl-pt-2">
<span class="badge badge-primary" data-testid="downstream-pipeline-label">{{ label }}</span>
</div>
</gl-deprecated-button>
</li>

View File

@ -41,6 +41,9 @@ export default {
onPipelineClick(downstreamNode, pipeline, index) {
this.$emit('linkedPipelineClick', pipeline, index, downstreamNode);
},
onDownstreamHovered(jobName) {
this.$emit('downstreamHovered', jobName);
},
},
};
</script>
@ -61,6 +64,7 @@ export default {
:column-title="columnTitle"
:project-id="projectId"
@pipelineClicked="onPipelineClick($event, pipeline, index)"
@downstreamHovered="onDownstreamHovered"
/>
</ul>
</div>

View File

@ -36,6 +36,11 @@ export default {
required: false,
default: () => ({}),
},
jobHovered: {
type: String,
required: false,
default: '',
},
},
computed: {
hasAction() {
@ -80,6 +85,7 @@ export default {
<job-item
v-if="group.size === 1"
:job="group.jobs[0]"
:job-hovered="jobHovered"
css-class-job-name="build-content"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>

View File

@ -43,7 +43,7 @@ export default {
<div v-if="hasSuites" class="test-reports-table gl-mb-3 js-test-cases-table">
<div role="row" class="gl-responsive-table-row table-row-header font-weight-bold fgray">
<div role="rowheader" class="table-section section-20">
{{ __('Class') }}
{{ __('Suite') }}
</div>
<div role="rowheader" class="table-section section-20">
{{ __('Name') }}
@ -70,7 +70,7 @@ export default {
class="gl-responsive-table-row rounded align-items-md-start mt-xs-3 js-case-row"
>
<div class="table-section section-20 section-wrap">
<div role="rowheader" class="table-mobile-header">{{ __('Class') }}</div>
<div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
<div
v-gl-tooltip
:title="testCase.classname"

View File

@ -85,7 +85,7 @@ export default {
<div class="row mt-2">
<div class="col-4 col-md">
<span class="js-total-tests">{{
sprintf(s__('TestReports|%{count} jobs'), { count: report.total_count })
sprintf(s__('TestReports|%{count} tests'), { count: report.total_count })
}}</span>
</div>

View File

@ -17,7 +17,7 @@ export default {
heading: {
type: String,
required: false,
default: s__('TestReports|Test suites'),
default: s__('TestReports|Jobs'),
},
},
computed: {
@ -47,7 +47,7 @@ export default {
<div v-if="hasSuites" class="test-reports-table gl-mb-3 js-test-suites-table">
<div role="row" class="gl-responsive-table-row table-row-header font-weight-bold">
<div role="rowheader" class="table-section section-25 pl-3">
{{ __('Suite') }}
{{ __('Job') }}
</div>
<div role="rowheader" class="table-section section-25">
{{ __('Duration') }}

View File

@ -21,7 +21,8 @@ export default {
props: {
summary: {
type: String,
required: true,
required: false,
default: '',
},
statusIcon: {
type: String,
@ -58,8 +59,8 @@ export default {
class="report-block-list-issue-description-text"
data-testid="test-summary-row-description"
>
{{ summary
}}<span v-if="popoverOptions" class="text-nowrap"
<slot name="summary">{{ summary }}</slot
><span v-if="popoverOptions" class="text-nowrap"
>&nbsp;<popover v-if="popoverOptions" :options="popoverOptions" class="align-top" />
</span>
</div>

View File

@ -109,6 +109,8 @@
padding: $gl-padding;
list-style: none;
transition: background $gl-transition-duration-medium $general-hover-transition-curve;
border-top-left-radius: $border-radius-default; // same border radius used by .bordered-box
border-top-right-radius: $border-radius-default;
a {
color: inherit;

View File

@ -1101,7 +1101,3 @@ button.mini-pipeline-graph-dropdown-toggle {
.progress-bar.bg-primary {
background-color: $blue-500 !important;
}
.parent-child-label-container {
padding-top: $gl-padding-4;
}

View File

@ -109,3 +109,7 @@
.gl-transition-property-stroke {
transition-property: stroke;
}
.gl-top-66vh {
top: 66vh;
}

View File

@ -8,7 +8,7 @@ module ApprovableBase
has_many :approved_by_users, through: :approvals, source: :user
end
def has_approved?(user)
def approved_by?(user)
return false unless user
approved_by_users.include?(user)

View File

@ -38,6 +38,11 @@ class Member < ApplicationRecord
scope: [:source_type, :source_id],
allow_nil: true
}
validates :user_id,
uniqueness: {
message: _('project bots cannot be added to other groups / projects')
},
if: :project_bot?
# This scope encapsulates (most of) the conditions a row in the member table
# must satisfy if it is a valid permission. Of particular note:
@ -473,6 +478,10 @@ class Member < ApplicationRecord
def update_highest_role_attribute
user_id
end
def project_bot?
user&.project_bot?
end
end
Member.prepend_if_ee('EE::Member')

View File

@ -17,14 +17,7 @@ class GroupMember < Member
scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
scope :of_ldap_type, -> { where(ldap: true) }
scope :count_users_by_group_id, -> do
if Feature.enabled?(:optimized_count_users_by_group_id)
group(:source_id).count
else
joins(:user).group(:source_id).count
end
end
scope :count_users_by_group_id, -> { group(:source_id).count }
after_create :update_two_factor_requirement, unless: :invite?
after_destroy :update_two_factor_requirement, unless: :invite?

View File

@ -22,6 +22,7 @@ class Namespace < ApplicationRecord
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics
has_one :namespace_settings, inverse_of: :namespace, class_name: 'NamespaceSetting', autosave: true
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class NamespaceSetting < ApplicationRecord
belongs_to :namespace, inverse_of: :namespace_settings
self.primary_key = :namespace_id
end
NamespaceSetting.prepend_if_ee('EE::NamespaceSetting')

View File

@ -1270,7 +1270,8 @@ class User < ApplicationRecord
namespace.path = username if username_changed?
namespace.name = name if name_changed?
else
build_namespace(path: username, name: name)
namespace = build_namespace(path: username, name: name)
namespace.build_namespace_settings
end
end

View File

@ -28,7 +28,11 @@ module Groups
@group.build_chat_team(name: response['name'], team_id: response['id'])
end
@group.add_owner(current_user) if @group.save
if @group.save
@group.add_owner(current_user)
add_settings_record
end
@group
end
@ -79,6 +83,10 @@ module Groups
params[:visibility_level] = Gitlab::CurrentSettings.current_application_settings.default_group_visibility
end
def add_settings_record
@group.create_namespace_settings
end
end
end

View File

@ -22,7 +22,7 @@ module Members
errors = []
members.each do |member|
if member.errors.any?
if member.invalid?
current_error =
# Invited users may not have an associated user
if member.user.present?

View File

@ -4,7 +4,7 @@ module MergeRequests
class RemoveApprovalService < MergeRequests::BaseService
# rubocop: disable CodeReuse/ActiveRecord
def execute(merge_request)
return unless merge_request.has_approved?(current_user)
return unless merge_request.approved_by?(current_user)
# paranoid protection against running wrong deletes
return unless merge_request.id && current_user.id

View File

@ -0,0 +1,5 @@
---
title: Add include_parent_milestones param to milestones API
merge_request: 36944
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Update GITLAB_WORKHORSE_VERSION to 8.37.0
merge_request: 36988
author:
type: other

View File

@ -0,0 +1,6 @@
---
title: Expands Jira integration to allow viewing and searching a list of of Jira issues
directly within GitLab
merge_request: 36435
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add namespace settings table
merge_request: 36321
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Make DAG annotations stick
merge_request: 37068
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add correlation between trigger job and child pipeline
merge_request: 36750
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix background overflow when design note is selected
merge_request: 36931
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Remove optimized_count_users_by_group_id feature flag
merge_request: 36953
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Fix displaying import errors from server
merge_request: 37073
author:
type: fixed

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class CreateNamespaceSettings < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
with_lock_retries do
create_table :namespace_settings, id: false do |t|
t.timestamps_with_timezone null: false
t.references :namespace, primary_key: true, default: nil, type: :integer, index: false, foreign_key: { on_delete: :cascade }
end
end
end
def down
drop_table :namespace_settings
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
class BackfillNamespaceSettings < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'BackfillNamespaceSettings'
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 10_000
disable_ddl_transaction!
class Namespace < ActiveRecord::Base
include EachBatch
self.table_name = 'namespaces'
end
def up
say "Scheduling `#{MIGRATION}` jobs"
queue_background_migration_jobs_by_range_at_intervals(Namespace, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
end
def down
# NOOP
end
end

View File

@ -13118,6 +13118,12 @@ CREATE TABLE public.namespace_root_storage_statistics (
snippets_size bigint DEFAULT 0 NOT NULL
);
CREATE TABLE public.namespace_settings (
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
namespace_id integer NOT NULL
);
CREATE TABLE public.namespace_statistics (
id integer NOT NULL,
namespace_id integer NOT NULL,
@ -17894,6 +17900,9 @@ ALTER TABLE ONLY public.namespace_limits
ALTER TABLE ONLY public.namespace_root_storage_statistics
ADD CONSTRAINT namespace_root_storage_statistics_pkey PRIMARY KEY (namespace_id);
ALTER TABLE ONLY public.namespace_settings
ADD CONSTRAINT namespace_settings_pkey PRIMARY KEY (namespace_id);
ALTER TABLE ONLY public.namespace_statistics
ADD CONSTRAINT namespace_statistics_pkey PRIMARY KEY (id);
@ -21776,6 +21785,9 @@ ALTER TABLE ONLY public.analytics_cycle_analytics_project_stages
ALTER TABLE ONLY public.issue_user_mentions
ADD CONSTRAINT fk_rails_3861d9fefa FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.namespace_settings
ADD CONSTRAINT fk_rails_3896d4fae5 FOREIGN KEY (namespace_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.self_managed_prometheus_alert_events
ADD CONSTRAINT fk_rails_3936dadc62 FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@ -23825,6 +23837,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200702201039
20200703064117
20200703121557
20200703124823
20200703125016
20200703154822
20200704143633
20200704161600

View File

@ -25,13 +25,14 @@ GET /projects/:id/milestones?search=version
Parameters:
| Attribute | Type | Required | Description |
| --------- | ------ | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iids[]` | integer array | optional | Return only the milestones having the given `iid` |
| `state` | string | optional | Return only `active` or `closed` milestones |
| `title` | string | optional | Return only the milestones having the given `title` |
| `search` | string | optional | Return only milestones with a title or description matching the provided string |
| Attribute | Type | Required | Description |
| ---------------------------- | ------ | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `iids[]` | integer array | optional | Return only the milestones having the given `iid`. Will be ignored if `include_parent_milestones` is set to `true` |
| `state` | string | optional | Return only `active` or `closed` milestones |
| `title` | string | optional | Return only the milestones having the given `title` |
| `search` | string | optional | Return only milestones with a title or description matching the provided string |
| `include_parent_milestones` | boolean | optional | Include milestones from parent group and ancestors. Introduced in [GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36944) |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/milestones"

View File

@ -9,7 +9,7 @@ pipeline](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6665).
```mermaid
graph TD
A["build-qa-image, gitlab:assets:compile pull-cache<br/>(canonical default refs only)"];
A["build-qa-image, compile-production-assets<br/>(canonical default refs only)"];
B[review-build-cng];
C[review-deploy];
D[CNG-mirror];
@ -44,23 +44,25 @@ subgraph "CNG-mirror pipeline"
### Detailed explanation
1. On every [pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) during the `test` stage, the
[`gitlab:assets:compile`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724487) job is automatically started.
- Once it's done, it starts the [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808)
manual job since the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) pipeline triggered in the
1. On every [pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) during the `prepare` stage, the
[`compile-production-assets`](https://gitlab.com/gitlab-org/gitlab/-/jobs/641770154) job is automatically started.
- Once it's done, the [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808)
job starts since the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) pipeline triggered in the
following step depends on it.
1. The [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808) job [triggers a pipeline](https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657)
1. Once `compile-production-assets` is done, the [`review-build-cng`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724808)
job [triggers a pipeline](https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657)
in the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) project.
- The `review-build-cng` job automatically starts only if your MR includes
[CI or frontend changes](../pipelines.md#changes-patterns). In other cases, the job is manual.
- The [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657) pipeline creates the Docker images of
each component (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.)
based on the commit from the [GitLab pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) and stores
them in its [registry](https://gitlab.com/gitlab-org/build/CNG-mirror/container_registry).
- We use the [`CNG-mirror`](https://gitlab.com/gitlab-org/build/CNG-mirror) project so that the `CNG`, (Cloud
Native GitLab), project's registry is not overloaded with a
lot of transient Docker images.
Native GitLab), project's registry is not overloaded with a lot of transient Docker images.
- Note that the official CNG images are built by the `cloud-native-image`
job, which runs only for tags, and triggers itself a [`CNG`](https://gitlab.com/gitlab-org/build/CNG) pipeline.
1. Once the `test` stage is done, the [`review-deploy`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724810) job
1. Once `review-build-cng` is done, the [`review-deploy`](https://gitlab.com/gitlab-org/gitlab/-/jobs/467724810) job
deploys the Review App using [the official GitLab Helm chart](https://gitlab.com/gitlab-org/charts/gitlab/) to
the [`review-apps`](https://console.cloud.google.com/kubernetes/clusters/details/us-central1-b/review-apps?project=gitlab-review-apps)
Kubernetes cluster on GCP.
@ -94,10 +96,9 @@ subgraph "CNG-mirror pipeline"
- The manual `review-stop` can be used to
stop a Review App manually, and is also started by GitLab once a merge
request's branch is deleted after being merged.
- The Kubernetes cluster is connected to the `gitlab-{ce,ee}` projects using
- The Kubernetes cluster is connected to the `gitlab` projects using
[GitLab's Kubernetes integration](../../user/project/clusters/index.md). This basically
allows to have a link to the Review App directly from the merge request
widget.
allows to have a link to the Review App directly from the merge request widget.
### Auto-stopping of Review Apps

View File

@ -26,6 +26,11 @@ all security features will be configured by default.
## Limitations
It is not possible to enable or disable a feature using the configuration page.
However, instructions on how to enable or disable a feature can be found through
the links next to each feature on that page.
It is not yet possible to enable or disable most features using the
configuration page. However, instructions on how to enable or disable a feature
can be found through the links next to each feature on that page.
If a project does not have an existing CI configuration, then the SAST feature
can be enabled by clicking on the "Enable with Merge Request" button under the
"Manage" column. Future work will expand this to editing _existing_ CI
configurations, and to other security features.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -32,7 +32,7 @@ You can enable container scanning by doing one of the following:
GitLab compares the found vulnerabilities between the source and target branches, and shows the
information directly in the merge request.
![Container Scanning Widget](img/container_scanning_v13_1.png)
![Container Scanning Widget](img/container_scanning_v13_2.png)
<!-- NOTE: The container scanning tool references the following heading in the code, so if you
make a change to this heading, make sure to update the documentation URLs used in the

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@ -36,7 +36,7 @@ NOTE: **Note:**
This comparison logic uses only the latest pipeline executed for the target branch's base commit.
Running the pipeline on any other commit has no effect on the merge request.
![DAST Widget](img/dast_all_v13_1.png)
![DAST Widget](img/dast_v13_2.png)
By clicking on one of the detected linked vulnerabilities, you can
see the details and the URL(s) affected.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -27,7 +27,7 @@ GitLab checks the Dependency Scanning report, compares the found vulnerabilities
between the source and target branches, and shows the information on the
merge request.
![Dependency Scanning Widget](img/dependency_scanning_v13_1.png)
![Dependency Scanning Widget](img/dependency_scanning_v13_2.png)
The results are sorted by the severity of the vulnerability:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -28,7 +28,7 @@ You can take advantage of SAST by doing one of the following:
GitLab checks the SAST report, compares the found vulnerabilities between the
source and target branches, and shows the information right on the merge request.
![SAST Widget](img/sast_v13_1.png)
![SAST Widget](img/sast_v13_2.png)
The results are sorted by the priority of the vulnerability:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -25,7 +25,7 @@ GitLab displays identified secrets as part of the SAST reports visibly in a few
- Pipelines' **Security** tab
- Report in the merge request widget
![Secret Detection in merge request widget](img/secret-detection-merge-request-ui.png)
![Secret Detection in merge request widget](img/secret_detection_v13_2.png)
## Use cases

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

View File

@ -17,7 +17,7 @@ for merging into production.
To access the Compliance Dashboard for a group, navigate to **{shield}** **Security & Compliance > Compliance** on the group's menu.
![Compliance Dashboard](img/compliance_dashboard_v12_10.png)
![Compliance Dashboard](img/compliance_dashboard_v13_2.png)
## Use cases
@ -27,6 +27,7 @@ You can use the dashboard to:
- Get an overview of the latest Merge Request for each project.
- See if Merge Requests were approved and by whom.
- See Merge Request authors.
- See the latest [CI Pipeline](../../../ci/pipelines/index.md) result for each Merge Request.
## Permissions

View File

@ -8,11 +8,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Iterations **(STARTER)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214713) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.1.
> - It's deployed behind a feature flag, disabled by default.
> - It's disabled on GitLab.com.
> - It's able to be enabled or disabled per-group
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-iterations-core-only). **(CORE ONLY)**
> - It was deployed behind a feature flag, disabled by default.
> - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/221047) on GitLab 13.2.
> - It's enabled on GitLab.com.
> - It's able to be enabled or disabled per-group.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#disable-iterations-core-only). **(CORE ONLY)**
Iterations are a way to track issues over a period of time. This allows teams
to track velocity and volatility metrics. Iterations can be used with [milestones](../../project/milestones/index.md)
@ -56,12 +57,11 @@ You need Developer [permissions](../../permissions.md) or higher to edit an iter
To edit an iteration, click the three-dot menu (**{ellipsis_v}**) > **Edit iteration**.
## Enable Iterations **(CORE ONLY)**
## Disable Iterations **(CORE ONLY)**
GitLab Iterations feature is under development and not ready for production use.
It is deployed behind a feature flag that is **disabled by default**.
GitLab Iterations feature is deployed with a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it for your instance. `:group_iterations` can be enabled or disabled per-group.
can disable it for your instance. `:group_iterations` can be enabled or disabled per-group.
To enable it:

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@ -55,29 +55,43 @@ In order to enable the Jira service in GitLab, you need to first configure the p
> **Notes:**
>
> - The currently supported Jira versions are `v6.x, v7.x, v8.x` . GitLab 7.8 or
> higher is required.
> - GitLab 8.14 introduced a new way to integrate with Jira which greatly simplified
> the configuration options you have to enter. If you are using an older version,
> [follow this documentation](https://gitlab.com/gitlab-org/gitlab/blob/8-13-stable-ee/doc/project_services/jira.md).
> - The supported Jira versions are `v6.x`, `v7.x`, and `v8.x`.
> - In order to support Oracle's Access Manager, GitLab will send additional cookies
> to enable Basic Auth. The cookie being added to each request is `OBBasicAuth` with
> a value of `fromDialog`.
To enable the Jira integration in a project, navigate to the
[Integrations page](overview.md#accessing-integrations), click
the **Jira** service, and fill in the required details on the page as described
in the table below.
[Integrations page](overview.md#accessing-integrations) and click
the **Jira** service.
Select **Enable integration**.
Select a **Trigger** action. This determines whether a mention of a Jira issue in GitLab commits, merge requests, or both, should link the Jira issue back to that source commit/MR and transition the Jira issue, if indicated.
To include a comment on the Jira issue when the above referene is made in GitLab, check **Enable comments**.
Enter the further details on the page as described in the following table.
| Field | Description |
| ----- | ----------- |
| `Web URL` | The base URL to the Jira instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
| `Jira API URL` | The base URL to the Jira instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. Leave this field blank (or use the same value of `Web URL`) if using **Jira Cloud**. |
| `Username/Email` | Created when [configuring Jira step](#configuring-jira). Use `username` for **Jira Server** or `email` for **Jira Cloud**. |
| `Password/API token` |Created in [configuring Jira step](#configuring-jira). Use `password` for **Jira Server** or `API token` for **Jira Cloud**. |
| `Transition ID` | This is the ID of a transition that moves issues to the desired state. It is possible to insert transition ids separated by `,` or `;` which means the issue will be moved to each state after another using the given order. **Closing Jira issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
| `Username or Email` | Created in [configuring Jira](#configuring-jira) step. Use `username` for **Jira Server** or `email` for **Jira Cloud**. |
| `Password/API token` |Created in [configuring Jira](#configuring-jira) step. Use `password` for **Jira Server** or `API token` for **Jira Cloud**. |
| `Transition ID` | Required for closing Jira issues via commits or merge requests. This is the ID of a transition in Jira that moves issues to a desired state. (See [Obtaining a transition ID](#obtaining-a-transition-id).) If you insert multiple transition IDs separated by `,` or `;`, the issue is moved to each state, one after another, using the given order. |
### Obtaining a transition ID
To enable users to view Jira issues inside GitLab, select **Enable Jira issues** and enter a project key. **(PREMIUM)**
CAUTION: **Caution:**
If you enable Jira issues with the setting above, all users that have access to this GitLab project will be able to view all issues from the specified Jira project.
When you have configured all settings, click **Test settings and save changes**.
Your GitLab project can now interact with all Jira projects in your instance and the project now displays a Jira link that opens the Jira project.
![Jira service page](img/jira_service_page_v12_2.png)
#### Obtaining a transition ID
In the most recent Jira user interface, you can no longer see transition IDs in the workflow
administration UI. You can get the ID you need in either of the following ways:
@ -90,19 +104,11 @@ administration UI. You can get the ID you need in either of the following ways:
Note that the transition ID may vary between workflows (e.g., bug vs. story),
even if the status you are changing to is the same.
After saving the configuration, your GitLab project will be able to interact
with all Jira projects in your Jira instance and you'll see the Jira link on the GitLab project pages that takes you to the appropriate Jira project.
#### Disabling comments on Jira issues
![Jira service page](img/jira_service_page_v12_2.png)
You can continue to have GitLab cross-link a source commit/MR with a Jira issue while disabling the comment added to the issue.
### Disabling comments on Jira issues
When you reference a Jira issue, it will always link back to the source commit/MR in GitLab, however, you can control whether GitLab will also cross-post a comment to the Jira issue. That functionality is enabled by default.
To disable the automated commenting on Jira issues:
1. Open the [Integrations page](overview.md#accessing-integrations) and select **Jira**.
1. In the **Event Action** section, uncheck **Comment**.
See the [Configuring GitLab](#configuring-gitlab) section and uncheck the **Enable comments** setting.
## Jira issues
@ -111,7 +117,7 @@ By now you should have [configured Jira](#configuring-jira) and enabled the
you should be able to reference and close Jira issues by just mentioning their
ID in GitLab commits and merge requests.
### Referencing Jira Issues
### Reference Jira issues
When GitLab project has Jira issue tracker configured and enabled, mentioning
Jira issue in GitLab will automatically add a comment in Jira issue with the
@ -138,7 +144,7 @@ For example, the following commit will reference the Jira issue with `PROJECT-1`
git commit -m "PROJECT-1 Fix spelling and grammar"
```
### Closing Jira Issues
### Close Jira issues
Jira issues can be closed directly from GitLab by using trigger words in
commits and merge requests. When a commit which contains the trigger word
@ -162,8 +168,6 @@ where `PROJECT-1` is the ID of the Jira issue.
> [project settings](img/jira_project_settings.png).
> - The Jira issue will not be transitioned if it has a resolution.
### Jira issue closing example
Let's consider the following example:
1. For the project named `PROJECT` in Jira, we implemented a new feature
@ -185,6 +189,45 @@ with a link to the commit that resolved the issue.
![The GitLab integration closes Jira issue](img/jira_service_close_issue.png)
### View Jira issues **(PREMIUM)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/3622) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
You can browse and search issues from a selected Jira project directly in GitLab. This requires [configuration](#configuring-gitlab) in GitLab by an administrator.
![Jira issues integration enabled](img/jira/open_jira_issues_list_v13.2.png)
From the **Jira Issues** menu, click **Issues List**. The issue list defaults to sort by **Created date**, with the newest issues listed at the top. You can change this to **Last updated**.
Issues are grouped into tabs based on their [Jira status](https://confluence.atlassian.com/adminjiraserver070/defining-status-field-values-749382903.html).
- The **Open** tab displays all issues with a Jira status in any category other than Done.
- The **Closed** tab displays all issues with a Jira status categorized as Done.
- The **All** tab displays all issues of any status.
Click an issue title to open its original Jira issue page for full details.
#### Search and filter the issues list
To refine the list of issues, use the search bar to search for any text
contained in an issue summary (title) or description.
You can also filter by labels, status, reporter, and assignee using URL parameters.
Enhancements to be able to use these through the user interface are [planned](https://gitlab.com/groups/gitlab-org/-/epics/3622).
- To filter issues by `labels`, specify one or more labels as part of the `labels[]`
parameter in the URL. When using multiple labels, only issues that contain all specified
labels are listed. `/-/integrations/jira/issues?labels[]=backend&labels[]=feature&labels[]=QA`
- To filter issues by `status`, specify the `status` parameter in the URL.
`/-/integrations/jira/issues?status=In Progress`
- To filter issues by `reporter`, specify a reporter's Jira display name for the
`author_username` parameter in the URL. `/-/integrations/jira/issues?author_username=John Smith`
- To filter issues by `assignee`, specify their Jira display name for the
`assignee_username` parameter in the URL. `/-/integrations/jira/issues?assignee_username=John Smith`
## Troubleshooting
If these features do not work as expected, it is likely due to a problem with the way the integration settings were configured.

View File

@ -4,11 +4,11 @@ module API
module Entities
class MergeRequestApprovals < Grape::Entity
expose :user_has_approved do |merge_request, options|
merge_request.has_approved?(options[:current_user])
merge_request.approved_by?(options[:current_user])
end
expose :user_can_approve do |merge_request, options|
!merge_request.has_approved?(options[:current_user]) &&
!merge_request.approved_by?(options[:current_user]) &&
options[:current_user].can?(:approve_merge_request, merge_request)
end

View File

@ -107,7 +107,7 @@ module API
if !member
not_allowed! # This currently can only be reached in EE
elsif member.persisted? && member.valid?
elsif member.valid? && member.persisted?
present_members(member)
else
render_validation_error!(member)

View File

@ -31,12 +31,14 @@ module API
end
def list_milestones_for(parent)
milestones = parent.milestones.order_id_desc
milestones = Milestone.filter_by_state(milestones, params[:state])
milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
milestones = filter_by_title(milestones, params[:title]) if params[:title]
finder_params = params.merge(milestones_finder_params(parent))
milestones = MilestonesFinder.new(finder_params).execute
milestones = filter_by_search(milestones, params[:search]) if params[:search]
if params[:iids].present? && !params[:include_parent_milestones]
milestones = filter_by_iid(milestones, params[:iids])
end
present paginate(milestones), with: Entities::Milestone
end
@ -96,6 +98,25 @@ module API
[MergeRequestsFinder, Entities::MergeRequestBasic]
end
end
def milestones_finder_params(parent)
if parent.is_a?(Group)
{ group_ids: parent.id }
else
{
project_ids: parent.id,
group_ids: parent_group_ids(parent)
}
end
end
def parent_group_ids(parent)
return unless params[:include_parent_milestones].present?
parent.group.self_and_ancestors
.public_or_visible_to_user(current_user)
.select(:id)
end
end
end
end

View File

@ -16,6 +16,8 @@ module API
end
params do
use :list_params
optional :include_parent_milestones, type: Boolean, default: false,
desc: 'Include milestones from parent group and ancestors'
end
get ":id/milestones" do
authorize! :read_milestone, user_project

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# Backfillnamespace_settings for a range of namespaces
class BackfillNamespaceSettings
def perform(start_id, end_id)
ActiveRecord::Base.connection.execute <<~SQL
INSERT INTO namespace_settings (namespace_id, created_at, updated_at)
SELECT namespaces.id, now(), now()
FROM namespaces
WHERE namespaces.id BETWEEN #{start_id} AND #{end_id}
ON CONFLICT (namespace_id) DO NOTHING;
SQL
end
end
end
end

View File

@ -6,6 +6,7 @@ module Gitlab
module Danger
module Helper
RELEASE_TOOLS_BOT = 'gitlab-release-tools-bot'
DRAFT_REGEX = /\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
# Returns a list of all files that have been added, modified or renamed.
# `git.modified_files` might contain paths that already have been renamed,
@ -210,7 +211,7 @@ module Gitlab
end
def sanitize_mr_title(title)
title.gsub(/^WIP: */, '').gsub(/`/, '\\\`')
title.gsub(DRAFT_REGEX, '').gsub(/`/, '\\\`')
end
def security_mr?

View File

@ -568,25 +568,25 @@ msgstr[1] ""
msgid "%{remaining_approvals} left"
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities out of %{total}."
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}."
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical and %{high} high severity vulnerabilities."
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} and %{highStart}%{high} new high%{highEnd} severity vulnerabilities."
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerabilities out of %{total}."
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities out of %{total}."
msgstr ""
msgid "%{reportType} %{status} detected %{critical} critical severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{critical} critical severity vulnerabilities."
msgid "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{criticalStart}%{critical} new critical%{criticalEnd} severity vulnerabilities."
msgstr[0] ""
msgstr[1] ""
msgid "%{reportType} %{status} detected %{high} high severity vulnerabilities out of %{total}."
msgid "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities out of %{total}."
msgstr ""
msgid "%{reportType} %{status} detected %{high} high severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{high} high severity vulnerabilities."
msgid "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerability."
msgid_plural "%{reportType} %{status} detected %{highStart}%{high} new high%{highEnd} severity vulnerabilities."
msgstr[0] ""
msgstr[1] ""
@ -4782,9 +4782,6 @@ msgstr ""
msgid "CiVariable|Validation failed"
msgstr ""
msgid "Class"
msgstr ""
msgid "Classification Label (optional)"
msgstr ""
@ -6113,6 +6110,9 @@ msgstr ""
msgid "Complete"
msgstr ""
msgid "Completed"
msgstr ""
msgid "Compliance"
msgstr ""
@ -7001,6 +7001,9 @@ msgstr ""
msgid "Created branch '%{branch_name}' and a merge request to resolve this issue."
msgstr ""
msgid "Created by %{job}"
msgstr ""
msgid "Created by me"
msgstr ""
@ -12521,6 +12524,9 @@ msgstr ""
msgid "ImportProjects|Importing the project failed"
msgstr ""
msgid "ImportProjects|Importing the project failed: %{reason}"
msgstr ""
msgid "ImportProjects|Requesting your %{provider} repositories failed"
msgstr ""
@ -14393,6 +14399,9 @@ msgstr ""
msgid "Maximum job timeout has a value which could not be accepted"
msgstr ""
msgid "Maximum length 100 characters"
msgstr ""
msgid "Maximum lifetime allowable for Personal Access Tokens is active, your expire date must be set before %{maximum_allowable_date}."
msgstr ""
@ -15321,6 +15330,9 @@ msgstr ""
msgid "MrDeploymentActions|Stop environment"
msgstr ""
msgid "Multi-project"
msgstr ""
msgid "Multiple IP address ranges are supported."
msgstr ""
@ -15360,6 +15372,9 @@ msgstr ""
msgid "Name has already been taken"
msgstr ""
msgid "Name is required"
msgstr ""
msgid "Name new label"
msgstr ""
@ -15752,10 +15767,10 @@ msgstr ""
msgid "No grouping"
msgstr ""
msgid "No iteration"
msgid "No issues found"
msgstr ""
msgid "No iterations found"
msgid "No iteration"
msgstr ""
msgid "No iterations to show"
@ -16693,6 +16708,33 @@ msgstr ""
msgid "Page was successfully deleted"
msgstr ""
msgid "PagerDutySettings|Active"
msgstr ""
msgid "PagerDutySettings|Create a GitLab issue for each PagerDuty incident by %{docsLink}"
msgstr ""
msgid "PagerDutySettings|Failed to update Webhook URL"
msgstr ""
msgid "PagerDutySettings|Reset webhook URL"
msgstr ""
msgid "PagerDutySettings|Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty."
msgstr ""
msgid "PagerDutySettings|Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident."
msgstr ""
msgid "PagerDutySettings|Webhook URL"
msgstr ""
msgid "PagerDutySettings|Webhook URL update was successful"
msgstr ""
msgid "PagerDutySettings|configuring a webhook in PagerDuty"
msgstr ""
msgid "Pages"
msgstr ""
@ -23179,13 +23221,13 @@ msgstr ""
msgid "TestReports|%{count} failures"
msgstr ""
msgid "TestReports|%{count} jobs"
msgid "TestReports|%{count} tests"
msgstr ""
msgid "TestReports|%{rate}%{sign} success rate"
msgstr ""
msgid "TestReports|Test suites"
msgid "TestReports|Jobs"
msgstr ""
msgid "TestReports|Tests"
@ -24221,12 +24263,6 @@ msgstr ""
msgid "This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b>"
msgstr ""
msgid "This pipeline triggered a child pipeline"
msgstr ""
msgid "This pipeline was triggered by a parent pipeline"
msgstr ""
msgid "This pipeline was triggered by a schedule."
msgstr ""
@ -27581,6 +27617,9 @@ msgstr ""
msgid "cannot merge"
msgstr ""
msgid "child-pipeline"
msgstr ""
msgid "ciReport|%{degradedNum} degraded"
msgstr ""
@ -28609,6 +28648,9 @@ msgstr ""
msgid "project avatar"
msgstr ""
msgid "project bots cannot be added to other groups / projects"
msgstr ""
msgid "project is read-only"
msgstr ""

View File

@ -2,7 +2,7 @@
export SETUP_DB=${SETUP_DB:-true}
export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true}
export BUNDLE_INSTALL_FLAGS="--without=production --without=development --jobs=$(nproc) --path=vendor --retry=3 --quiet"
export BUNDLE_INSTALL_FLAGS=${BUNDLE_INSTALL_FLAGS:-"--without=production --without=development --jobs=$(nproc) --path=vendor --retry=3 --quiet"}
if [ "$USE_BUNDLE_INSTALL" != "false" ]; then
bundle --version

View File

@ -106,6 +106,29 @@ RSpec.describe Projects::ProjectMembersController do
expect(response).to redirect_to(project_project_members_path(project))
end
end
context 'adding project bot' do
let_it_be(:project_bot) { create(:user, :project_bot) }
before do
project.add_maintainer(user)
unrelated_project = create(:project)
unrelated_project.add_maintainer(project_bot)
end
it 'returns error' do
post :create, params: {
namespace_id: project.namespace,
project_id: project,
user_ids: project_bot.id,
access_level: Gitlab::Access::GUEST
}
expect(flash[:alert]).to include('project bots cannot be added to other groups / projects')
expect(response).to redirect_to(project_project_members_path(project))
end
end
end
describe 'PUT update' do

View File

@ -384,7 +384,7 @@ RSpec.describe 'Pipeline', :js do
find('.js-tests-tab-link').click
wait_for_requests
expect(page).to have_content('Test suites')
expect(page).to have_content('Jobs')
expect(page).to have_selector('.js-tests-detail', visible: :all)
end
end
@ -412,7 +412,7 @@ RSpec.describe 'Pipeline', :js do
it 'calls summary.json endpoint', :js do
find('.js-tests-tab-link').click
expect(page).to have_content('Test suites')
expect(page).to have_content('Jobs')
expect(page).to have_selector('.js-tests-detail', visible: :all)
end
end

View File

@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import createFlash from '~/flash';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
@ -22,6 +23,8 @@ import {
} from '~/import_projects/store/actions';
import state from '~/import_projects/store/state';
jest.mock('~/flash');
describe('import_projects store actions', () => {
let localState;
const repos = [{ id: 1 }, { id: 2 }];
@ -130,10 +133,10 @@ describe('import_projects store actions', () => {
);
});
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR on an unsuccessful request', () => {
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows generic error message on an unsuccessful request', async () => {
mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500);
return testAction(
await testAction(
fetchImport,
importPayload,
localState,
@ -143,6 +146,26 @@ describe('import_projects store actions', () => {
],
[],
);
expect(createFlash).toHaveBeenCalledWith('Importing the project failed');
});
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows detailed error message on an unsuccessful request with errors fields in response', async () => {
const ERROR_MESSAGE = 'dummy';
mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500, { errors: ERROR_MESSAGE });
await testAction(
fetchImport,
importPayload,
localState,
[
{ type: REQUEST_IMPORT, payload: importPayload.repo.id },
{ type: RECEIVE_IMPORT_ERROR, payload: importPayload.repo.id },
],
[],
);
expect(createFlash).toHaveBeenCalledWith(`Importing the project failed: ${ERROR_MESSAGE}`);
});
});

View File

@ -48,7 +48,14 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
data-testid="AlertsSettingsForm-tab"
/>
</gl-tab-stub>
<!---->
<gl-tab-stub
title="PagerDuty integration"
>
<pagerdutysettingsform-stub
class="gl-pt-3"
data-testid="PagerDutySettingsForm-tab"
/>
</gl-tab-stub>
<!---->
</gl-tabs-stub>
</div>

View File

@ -0,0 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Alert integration settings form should match the default snapshot 1`] = `
<div>
<!---->
<p>
Setting up a webhook with PagerDuty will automatically create a GitLab issue for each PagerDuty incident.
</p>
<form>
<gl-form-group-stub
class="col-8 col-md-9 gl-p-0"
>
<gl-toggle-stub
id="active"
label="Active"
labelposition="top"
value="true"
/>
</gl-form-group-stub>
<gl-form-group-stub
class="col-8 col-md-9 gl-p-0"
label="Webhook URL"
label-class="label-bold"
label-for="url"
>
<gl-form-input-group-stub
data-testid="webhook-url"
id="url"
predefinedoptions="[object Object]"
readonly=""
value="pagerduty.webhook.com"
/>
<div
class="gl-text-gray-400 gl-pt-2"
>
<gl-sprintf-stub
message="Create a GitLab issue for each PagerDuty incident by %{docsLink}"
/>
</div>
<gl-button-stub
category="tertiary"
class="gl-mt-3"
data-testid="webhook-reset-btn"
icon=""
role="button"
size="medium"
tabindex="0"
variant="default"
>
Reset webhook URL
</gl-button-stub>
<gl-modal-stub
modalclass=""
modalid="resetWebhookModal"
ok-title="Reset webhook URL"
ok-variant="danger"
size="md"
title="Reset webhook URL"
titletag="h4"
>
Resetting the webhook URL for this project will require updating this integration's settings in PagerDuty.
</gl-modal-stub>
</gl-form-group-stub>
<gl-button-stub
category="tertiary"
class="js-no-auto-disable"
icon=""
size="medium"
type="submit"
variant="success"
>
Save changes
</gl-button-stub>
</form>
</div>
`;

View File

@ -1,24 +1,16 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import AlertsSettingsForm from '~/incidents_settings/components/alerts_form.vue';
import { ERROR_MSG } from '~/incidents_settings/constants';
import createFlash from '~/flash';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('Alert integration settings form', () => {
let wrapper;
const service = { updateSettings: jest.fn().mockResolvedValue() };
const findForm = () => wrapper.find({ ref: 'settingsForm' });
beforeEach(() => {
wrapper = shallowMount(AlertsSettingsForm, {
provide: {
operationsSettingsEndpoint: 'operations/endpoint',
service,
alertSettings: {
issueTemplateKey: 'selecte_tmpl',
createIssue: true,
@ -32,6 +24,7 @@ describe('Alert integration settings form', () => {
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
@ -42,30 +35,15 @@ describe('Alert integration settings form', () => {
});
describe('form', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
it('should refresh the page on successful submit', () => {
mock.onPatch().reply(200);
it('should call service `updateSettings` on submit', () => {
findForm().trigger('submit');
return waitForPromises().then(() => {
expect(refreshCurrentPage).toHaveBeenCalled();
});
});
it('should display a flah message on unsuccessful submit', () => {
mock.onPatch().reply(400);
findForm().trigger('submit');
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(ERROR_MSG), 'alert');
});
expect(service.updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
create_issue: wrapper.vm.createIssueEnabled,
issue_template_key: wrapper.vm.issueTemplate,
send_email: wrapper.vm.sendEmailEnabled,
}),
);
});
});
});

View File

@ -0,0 +1,55 @@
import axios from '~/lib/utils/axios_utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import httpStatusCodes from '~/lib/utils/http_status';
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import { ERROR_MSG } from '~/incidents_settings/constants';
import createFlash from '~/flash';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('IncidentsSettingsService', () => {
const settingsEndpoint = 'operations/settings';
const webhookUpdateEndpoint = 'webhook/update';
let mock;
let service;
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
service = new IncidentsSettingsService(settingsEndpoint, webhookUpdateEndpoint);
});
afterEach(() => {
mock.restore();
});
describe('updateSettings', () => {
it('should refresh the page on successful update', () => {
mock.onPatch().reply(httpStatusCodes.OK);
return service.updateSettings({}).then(() => {
expect(refreshCurrentPage).toHaveBeenCalled();
});
});
it('should display a flash message on update error', () => {
mock.onPatch().reply(httpStatusCodes.BAD_REQUEST);
return service.updateSettings({}).then(() => {
expect(createFlash).toHaveBeenCalledWith(expect.stringContaining(ERROR_MSG), 'alert');
});
});
});
describe('resetWebhookUrl', () => {
it('should make a call for webhook update', () => {
jest.spyOn(axios, 'post');
mock.onPost().reply(httpStatusCodes.OK);
return service.resetWebhookUrl().then(() => {
expect(axios.post).toHaveBeenCalledWith(webhookUpdateEndpoint);
});
});
});
});

View File

@ -6,7 +6,9 @@ describe('IncidentsSettingTabs', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(IncidentsSettingTabs);
wrapper = shallowMount(IncidentsSettingTabs, {
provide: { glFeatures: { pagerdutyWebhook: true } },
});
});
afterEach(() => {

View File

@ -0,0 +1,67 @@
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import PagerDutySettingsForm from '~/incidents_settings/components/pagerduty_form.vue';
import { GlAlert, GlModal } from '@gitlab/ui';
describe('Alert integration settings form', () => {
let wrapper;
const resetWebhookUrl = jest.fn();
const service = { updateSettings: jest.fn().mockResolvedValue(), resetWebhookUrl };
const findForm = () => wrapper.find({ ref: 'settingsForm' });
const findWebhookInput = () => wrapper.find('[data-testid="webhook-url"]');
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
beforeEach(() => {
wrapper = shallowMount(PagerDutySettingsForm, {
provide: {
service,
pagerDutySettings: {
active: true,
webhookUrl: 'pagerduty.webhook.com',
webhookUpdateEndpoint: 'webhook/update',
},
},
});
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
it('should match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('should call service `updateSettings` on form submit', () => {
findForm().trigger('submit');
expect(service.updateSettings).toHaveBeenCalledWith(
expect.objectContaining({ pagerduty_active: wrapper.vm.active }),
);
});
describe('Webhook reset', () => {
it('should make a call for webhook reset and reset form values', async () => {
const newWebhookUrl = 'new.webhook.url?token=token';
resetWebhookUrl.mockResolvedValueOnce({
data: { pagerduty_webhook_url: newWebhookUrl },
});
findModal().vm.$emit('ok');
await waitForPromises();
expect(resetWebhookUrl).toHaveBeenCalled();
expect(findWebhookInput().attributes('value')).toBe(newWebhookUrl);
expect(findAlert().attributes('variant')).toBe('success');
});
it('should show error message and NOT reset webhook url', async () => {
resetWebhookUrl.mockRejectedValueOnce();
findModal().vm.$emit('ok');
await waitForPromises();
expect(findAlert().attributes('variant')).toBe('danger');
});
});
});

View File

@ -5,6 +5,8 @@ import JobItem from '~/pipelines/components/graph/job_item.vue';
describe('pipeline graph job item', () => {
let wrapper;
const findJobWithoutLink = () => wrapper.find('[data-testid="job-without-link"]');
const createWrapper = propsData => {
wrapper = mount(JobItem, {
propsData,
@ -57,7 +59,7 @@ describe('pipeline graph job item', () => {
});
describe('name without link', () => {
it('it should render status and name', () => {
beforeEach(() => {
createWrapper({
job: {
id: 4257,
@ -71,13 +73,22 @@ describe('pipeline graph job item', () => {
has_details: false,
},
},
cssClassJobName: 'css-class-job-name',
jobHovered: 'test',
});
});
it('it should render status and name', () => {
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
expect(wrapper.find('a').exists()).toBe(false);
expect(trimText(wrapper.find('.ci-status-text').text())).toEqual(mockJob.name);
});
it('should apply hover class and provided class name', () => {
expect(findJobWithoutLink().classes()).toContain('gl-inset-border-1-blue-500');
expect(findJobWithoutLink().classes()).toContain('css-class-job-name');
});
});
describe('action icon', () => {

View File

@ -11,7 +11,10 @@ const invalidTriggeredPipelineId = mockPipeline.project.id + 5;
describe('Linked pipeline', () => {
let wrapper;
const findButton = () => wrapper.find('button');
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
const createWrapper = propsData => {
wrapper = mount(LinkedPipelineComponent, {
@ -69,6 +72,8 @@ describe('Linked pipeline', () => {
it('should correctly compute the tooltip text', () => {
expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.details.status.label);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.source_job.name);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.id);
});
it('should render the tooltip text as the title attribute', () => {
@ -83,9 +88,8 @@ describe('Linked pipeline', () => {
expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(false);
});
it('should not display child label when pipeline project id is not the same as triggered pipeline project id', () => {
const labelContainer = wrapper.find('.parent-child-label-container');
expect(labelContainer.exists()).toBe(false);
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
expect(findPipelineLabel().text()).toBe('Multi-project');
});
});
@ -103,17 +107,17 @@ describe('Linked pipeline', () => {
it('parent/child label container should exist', () => {
createWrapper(downstreamProps);
expect(wrapper.find('.parent-child-label-container').exists()).toBe(true);
expect(findPipelineLabel().exists()).toBe(true);
});
it('should display child label when pipeline project id is the same as triggered pipeline project id', () => {
createWrapper(downstreamProps);
expect(wrapper.find('.parent-child-label-container').text()).toContain('Child');
expect(findPipelineLabel().exists()).toBe(true);
});
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
createWrapper(upstreamProps);
expect(wrapper.find('.parent-child-label-container').text()).toContain('Parent');
expect(findPipelineLabel().exists()).toBe(true);
});
});
@ -133,7 +137,7 @@ describe('Linked pipeline', () => {
});
});
describe('on click', () => {
describe('on click/hover', () => {
const props = {
pipeline: mockPipeline,
projectId: validTriggeredPipelineId,
@ -160,5 +164,15 @@ describe('Linked pipeline', () => {
'js-linked-pipeline-34993051',
]);
});
it('should emit downstreamHovered with job name on mouseover', () => {
findLinkedPipeline().trigger('mouseover');
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['trigger_job']]);
});
it('should emit downstreamHovered with empty string on mouseleave', () => {
findLinkedPipeline().trigger('mouseleave');
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['']]);
});
});
});

View File

@ -14,6 +14,9 @@ export default {
active: false,
coverage: null,
source: 'push',
source_job: {
name: 'trigger_job',
},
created_at: '2018-06-05T11:31:30.452Z',
updated_at: '2018-10-31T16:35:31.305Z',
path: '/gitlab-org/gitlab-runner/pipelines/23211253',
@ -381,6 +384,9 @@ export default {
active: false,
coverage: null,
source: 'pipeline',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-com/gitlab-docs/pipelines/34993051',
details: {
status: {
@ -889,6 +895,9 @@ export default {
active: false,
coverage: null,
source: 'pipeline',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-com/gitlab-docs/pipelines/34993051',
details: {
status: {
@ -1402,6 +1411,9 @@ export default {
active: false,
coverage: null,
source: 'pipeline',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-com/gitlab-docs/pipelines/34993051',
details: {
status: {
@ -1912,6 +1924,9 @@ export default {
active: false,
coverage: null,
source: 'pipeline',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-com/gitlab-docs/pipelines/34993051',
details: {
status: {
@ -2412,6 +2427,9 @@ export default {
active: false,
coverage: null,
source: 'push',
source_job: {
name: 'trigger_job',
},
created_at: '2019-01-06T17:48:37.599Z',
updated_at: '2019-01-06T17:48:38.371Z',
path: '/h5bp/html5-boilerplate/pipelines/26',
@ -3743,6 +3761,9 @@ export default {
active: false,
coverage: null,
source: 'push',
source_job: {
name: 'trigger_job',
},
path: '/gitlab-org/gitlab-test/pipelines/4',
details: {
status: {

View File

@ -60,7 +60,7 @@ describe('Test reports summary', () => {
});
it('displays the correct total', () => {
expect(totalTests().text()).toBe('4 jobs');
expect(totalTests().text()).toBe('4 tests');
});
it('displays the correct failure count', () => {

View File

@ -1,10 +1,8 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import component from '~/reports/components/summary_row.vue';
import { mount } from '@vue/test-utils';
import SummaryRow from '~/reports/components/summary_row.vue';
describe('Summary row', () => {
const Component = Vue.extend(component);
let vm;
let wrapper;
const props = {
summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability',
@ -15,23 +13,42 @@ describe('Summary row', () => {
statusIcon: 'warning',
};
beforeEach(() => {
vm = mountComponent(Component, props);
});
const createComponent = ({ propsData = {}, slots = {} } = {}) => {
wrapper = mount(SummaryRow, {
propsData: {
...props,
...propsData,
},
slots,
});
};
const findSummary = () => wrapper.find('.report-block-list-issue-description-text');
afterEach(() => {
vm.$destroy();
wrapper.destroy();
wrapper = null;
});
it('renders provided summary', () => {
expect(
vm.$el.querySelector('.report-block-list-issue-description-text').textContent.trim(),
).toEqual(props.summary);
createComponent();
expect(findSummary().text()).toEqual(props.summary);
});
it('renders provided icon', () => {
expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain(
createComponent();
expect(wrapper.find('.report-block-list-icon span').classes()).toContain(
'js-ci-status-icon-warning',
);
});
describe('summary slot', () => {
it('replaces the summary prop', () => {
const summarySlotContent = 'Summary slot content';
createComponent({ slots: { summary: summarySlotContent } });
expect(wrapper.text()).not.toContain(props.summary);
expect(findSummary().text()).toEqual(summarySlotContent);
});
});
});

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceSettings, schema: 20200703125016 do
let(:namespaces) { table(:namespaces) }
let(:namespace_settings) { table(:namespace_settings) }
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
subject { described_class.new }
describe '#perform' do
it 'creates settings for all projects in range' do
namespaces.create!(id: 5, name: 'test1', path: 'test1')
namespaces.create!(id: 7, name: 'test2', path: 'test2')
namespaces.create!(id: 8, name: 'test3', path: 'test3')
subject.perform(5, 7)
expect(namespace_settings.all.pluck(:namespace_id)).to contain_exactly(5, 7)
end
end
end

View File

@ -24,7 +24,15 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat
confirmed_at: 1.day.ago)
end
let(:migration_bot) { User.migration_bot }
let!(:migration_bot) do
users.create(id: 100,
email: "noreply+gitlab-migration-bot%s@#{Settings.gitlab.host}",
user_type: HasUserType::USER_TYPES[:migration_bot],
name: 'GitLab Migration Bot',
projects_limit: 10,
username: 'bot')
end
let!(:snippet_with_repo) { snippets.create(id: 1, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
let!(:snippet_with_empty_repo) { snippets.create(id: 2, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
let!(:snippet_without_repo) { snippets.create(id: 3, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }

View File

@ -363,6 +363,11 @@ RSpec.describe Gitlab::Danger::Helper do
where(:mr_title, :expected_mr_title) do
'My MR title' | 'My MR title'
'WIP: My MR title' | 'My MR title'
'Draft: My MR title' | 'My MR title'
'(Draft) My MR title' | 'My MR title'
'[Draft] My MR title' | 'My MR title'
'[DRAFT] My MR title' | 'My MR title'
'DRAFT: My MR title' | 'My MR title'
end
with_them do

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200703125016_backfill_namespace_settings.rb')
RSpec.describe BackfillNamespaceSettings, :sidekiq, schema: 20200703124823 do
let(:namespaces) { table(:namespaces) }
describe '#up' do
before do
stub_const("#{described_class}::BATCH_SIZE", 2)
namespaces.create!(id: 1, name: 'test1', path: 'test1')
namespaces.create!(id: 2, name: 'test2', path: 'test2')
namespaces.create!(id: 3, name: 'test3', path: 'test3')
end
it 'schedules BackfillNamespaceSettings background jobs' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, 1, 2)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, 3, 3)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
end
end
end

View File

@ -3,11 +3,11 @@
require 'spec_helper'
RSpec.describe ApprovableBase do
describe '#has_approved?' do
describe '#approved_by?' do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
subject { merge_request.has_approved?(user) }
subject { merge_request.approved_by?(user) }
context 'when a user has not approved' do
it 'returns false' do

View File

@ -88,6 +88,28 @@ RSpec.describe Member do
expect(child_member).to be_valid
end
end
context 'project bots' do
let_it_be(:project_bot) { create(:user, :project_bot) }
let(:new_member) { build(:project_member, user_id: project_bot.id) }
context 'not a member of any group or project' do
it 'is valid' do
expect(new_member).to be_valid
end
end
context 'already member of a project' do
before do
unrelated_project = create(:project)
unrelated_project.add_maintainer(project_bot)
end
it 'is not valid' do
expect(new_member).not_to be_valid
end
end
end
end
describe 'Scopes & finders' do

View File

@ -4,50 +4,18 @@ require 'spec_helper'
RSpec.describe GroupMember do
context 'scopes' do
shared_examples '.count_users_by_group_id' do
it 'counts users by group ID' do
user_1 = create(:user)
user_2 = create(:user)
group_1 = create(:group)
group_2 = create(:group)
it 'counts users by group ID' do
user_1 = create(:user)
user_2 = create(:user)
group_1 = create(:group)
group_2 = create(:group)
group_1.add_owner(user_1)
group_1.add_owner(user_2)
group_2.add_owner(user_1)
group_1.add_owner(user_1)
group_1.add_owner(user_2)
group_2.add_owner(user_1)
expect(described_class.count_users_by_group_id).to eq(group_1.id => 2,
group_2.id => 1)
end
end
describe '.count_users_by_group_id with optimized_count_users_by_group_id feature flag on' do
before do
stub_feature_flags(optimized_count_users_by_group_id: true)
end
it_behaves_like '.count_users_by_group_id'
it 'does not JOIN users' do
scope = described_class.all
expect(scope).not_to receive(:joins).with(:user)
scope.count_users_by_group_id
end
end
describe '.count_users_by_group_id with optimized_count_users_by_group_id feature flag off' do
before do
stub_feature_flags(optimized_count_users_by_group_id: false)
end
it_behaves_like '.count_users_by_group_id'
it 'does JOIN users' do
scope = described_class.all
expect(scope).to receive(:joins).with(:user).and_call_original
scope.count_users_by_group_id
end
expect(described_class.count_users_by_group_id).to eq(group_1.id => 2,
group_2.id => 1)
end
describe '.of_ldap_type' do

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe NamespaceSetting, type: :model do
it { is_expected.to belong_to(:namespace) }
end

View File

@ -17,6 +17,7 @@ RSpec.describe Namespace do
it { is_expected.to have_many :children }
it { is_expected.to have_one :root_storage_statistics }
it { is_expected.to have_one :aggregation_schedule }
it { is_expected.to have_one :namespace_settings }
it { is_expected.to have_many :custom_emoji }
end

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