Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/reports.gitlab-ci.yml1
-rw-r--r--.gitlab/ci/setup.gitlab-ci.yml1
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/import_projects/store/actions.js15
-rw-r--r--app/assets/javascripts/incidents_settings/components/alerts_form.vue25
-rw-r--r--app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue14
-rw-r--r--app/assets/javascripts/incidents_settings/components/pagerduty_form.vue183
-rw-r--r--app/assets/javascripts/incidents_settings/constants.js38
-rw-r--r--app/assets/javascripts/incidents_settings/incidents_settings_service.js32
-rw-r--r--app/assets/javascripts/incidents_settings/index.js19
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag_annotations.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue52
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue6
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue4
-rw-r--r--app/assets/javascripts/reports/components/summary_row.vue7
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/utilities.scss4
-rw-r--r--app/models/concerns/approvable_base.rb2
-rw-r--r--app/models/member.rb9
-rw-r--r--app/models/members/group_member.rb9
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/namespace_setting.rb9
-rw-r--r--app/models/user.rb3
-rw-r--r--app/services/groups/create_service.rb10
-rw-r--r--app/services/members/create_service.rb2
-rw-r--r--app/services/merge_requests/remove_approval_service.rb2
-rw-r--r--changelogs/unreleased/196066-add-milestone-expired-info-be-2.yml5
-rw-r--r--changelogs/unreleased/217392-update-workhorse-version.yml5
-rw-r--r--changelogs/unreleased/224039-jira-issues-integration-doc.yml6
-rw-r--r--changelogs/unreleased/add-namespace-settings-table.yml5
-rw-r--r--changelogs/unreleased/dag-annotations-sticky.yml5
-rw-r--r--changelogs/unreleased/downstream-pipeline-ux.yml5
-rw-r--r--changelogs/unreleased/fix-design-note-border-radius.yml5
-rw-r--r--changelogs/unreleased/remove-ff-in-count-users-by-group.yml5
-rw-r--r--changelogs/unreleased/xanf-expose-bitbucket-error.yml5
-rw-r--r--db/migrate/20200703124823_create_namespace_settings.rb22
-rw-r--r--db/post_migrate/20200703125016_backfill_namespace_settings.rb29
-rw-r--r--db/structure.sql14
-rw-r--r--doc/api/milestones.md15
-rw-r--r--doc/development/testing_guide/review_apps.md25
-rw-r--r--doc/user/application_security/configuration/index.md11
-rw-r--r--doc/user/application_security/container_scanning/img/container_scanning_v13_1.pngbin31586 -> 0 bytes
-rw-r--r--doc/user/application_security/container_scanning/img/container_scanning_v13_2.pngbin0 -> 8658 bytes
-rw-r--r--doc/user/application_security/container_scanning/index.md2
-rw-r--r--doc/user/application_security/dast/img/dast_all_v13_1.pngbin29403 -> 0 bytes
-rw-r--r--doc/user/application_security/dast/img/dast_v13_2.pngbin0 -> 6763 bytes
-rw-r--r--doc/user/application_security/dast/index.md2
-rw-r--r--doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_1.pngbin38992 -> 0 bytes
-rw-r--r--doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_2.pngbin0 -> 10289 bytes
-rw-r--r--doc/user/application_security/dependency_scanning/index.md2
-rw-r--r--doc/user/application_security/sast/img/sast_v13_1.pngbin39325 -> 0 bytes
-rw-r--r--doc/user/application_security/sast/img/sast_v13_2.pngbin0 -> 7703 bytes
-rw-r--r--doc/user/application_security/sast/index.md2
-rw-r--r--doc/user/application_security/secret_detection/img/secret-detection-merge-request-ui.pngbin100409 -> 0 bytes
-rw-r--r--doc/user/application_security/secret_detection/img/secret_detection_v13_2.pngbin0 -> 5863 bytes
-rw-r--r--doc/user/application_security/secret_detection/index.md2
-rw-r--r--doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v12_10.pngbin98355 -> 0 bytes
-rw-r--r--doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_2.pngbin0 -> 84922 bytes
-rw-r--r--doc/user/compliance/compliance_dashboard/index.md3
-rw-r--r--doc/user/group/iterations/index.md18
-rw-r--r--doc/user/project/integrations/img/jira/open_jira_issues_list_v13.2.pngbin0 -> 130755 bytes
-rw-r--r--doc/user/project/integrations/jira.md97
-rw-r--r--lib/api/entities/merge_request_approvals.rb4
-rw-r--r--lib/api/members.rb2
-rw-r--r--lib/api/milestone_responses.rb29
-rw-r--r--lib/api/project_milestones.rb2
-rw-r--r--lib/gitlab/background_migration/backfill_namespace_settings.rb18
-rw-r--r--lib/gitlab/danger/helper.rb3
-rw-r--r--locale/gitlab.pot84
-rw-r--r--scripts/prepare_build.sh2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb23
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb4
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js27
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap9
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap89
-rw-r--r--spec/frontend/incidents_settings/components/alerts_form_spec.js44
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js55
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js4
-rw-r--r--spec/frontend/incidents_settings/components/pagerduty_form_spec.js67
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js13
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js28
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_mock_data.js21
-rw-r--r--spec/frontend/pipelines/test_reports/test_summary_spec.js2
-rw-r--r--spec/frontend/reports/components/summary_row_spec.js43
-rw-r--r--spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb23
-rw-r--r--spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb10
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb5
-rw-r--r--spec/migrations/20200703125016_backfill_namespace_settings_spec.rb30
-rw-r--r--spec/models/concerns/approvable_base_spec.rb4
-rw-r--r--spec/models/member_spec.rb22
-rw-r--r--spec/models/members/group_member_spec.rb56
-rw-r--r--spec/models/namespace_setting_spec.rb7
-rw-r--r--spec/models/namespace_spec.rb1
-rw-r--r--spec/models/user_spec.rb6
-rw-r--r--spec/requests/api/group_import_spec.rb1
-rw-r--r--spec/requests/api/members_spec.rb74
-rw-r--r--spec/requests/api/project_milestones_spec.rb67
-rw-r--r--spec/services/groups/create_service_spec.rb9
105 files changed, 1361 insertions, 317 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 249bb35ec56..a5b80c7ca55 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -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:
diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml
index 0db4ae425e1..228747ae8d3 100644
--- a/.gitlab/ci/reports.gitlab-ci.yml
+++ b/.gitlab/ci/reports.gitlab-ci.yml
@@ -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
diff --git a/.gitlab/ci/setup.gitlab-ci.yml b/.gitlab/ci/setup.gitlab-ci.yml
index b878bec3751..26c7a2194cc 100644
--- a/.gitlab/ci/setup.gitlab-ci.yml
+++ b/.gitlab/ci/setup.gitlab-ci.yml
@@ -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
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index be23c04bf4c..2554e8ae98f 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.36.0
+8.37.0
diff --git a/app/assets/javascripts/import_projects/store/actions.js b/app/assets/javascripts/import_projects/store/actions.js
index 2422a1ed2e4..8d8d33f5972 100644
--- a/app/assets/javascripts/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_projects/store/actions.js
@@ -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);
});
diff --git a/app/assets/javascripts/incidents_settings/components/alerts_form.vue b/app/assets/javascripts/incidents_settings/components/alerts_form.vue
index 40eaef55c48..a394f404ee1 100644
--- a/app/assets/javascripts/incidents_settings/components/alerts_form.vue
+++ b/app/assets/javascripts/incidents_settings/components/alerts_form.vue
@@ -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;
+ });
},
},
};
diff --git a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
index 763568fd2c9..0623c275c5a 100644
--- a/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
+++ b/app/assets/javascripts/incidents_settings/components/incidents_settings_tabs.vue
@@ -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"
>
diff --git a/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
new file mode 100644
index 00000000000..027848db6e9
--- /dev/null
+++ b/app/assets/javascripts/incidents_settings/components/pagerduty_form.vue
@@ -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>
diff --git a/app/assets/javascripts/incidents_settings/constants.js b/app/assets/javascripts/incidents_settings/constants.js
index bd6ee55ae42..b443c237f0f 100644
--- a/app/assets/javascripts/incidents_settings/constants.js
+++ b/app/assets/javascripts/incidents_settings/constants.js
@@ -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.');
diff --git a/app/assets/javascripts/incidents_settings/incidents_settings_service.js b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
new file mode 100644
index 00000000000..bd4f5bb8820
--- /dev/null
+++ b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
@@ -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);
+ }
+}
diff --git a/app/assets/javascripts/incidents_settings/index.js b/app/assets/javascripts/incidents_settings/index.js
index 25fed0d10de..80e7d07feca 100644
--- a/app/assets/javascripts/incidents_settings/index.js
+++ b/app/assets/javascripts/incidents_settings/index.js
@@ -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);
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue b/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue
index dacaec6d36a..a1500166cdc 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue
@@ -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',
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 02fbf342ba7..6b890688a48 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -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
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index bfd314e0439..4d72cc55b34 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -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>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index 5aad49b05d1..733553e02c0 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -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');
- },
- childTooltipText() {
- return __('This pipeline was triggered by a parent pipeline');
+ if (this.parentPipeline) {
+ return __('Parent');
+ } else if (this.childPipeline) {
+ return __('Child');
+ }
+ return __('Multi-project');
},
- parentTooltipText() {
- return __('This pipeline triggered a child pipeline');
+ isDownstream() {
+ return this.columnTitle === __('Downstream');
},
- 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>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 8d99ce6704e..c4dfd3382a2 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -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>
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index bed0ed51d5f..9de6ba819c2 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -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"
/>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index dee82eb5c42..d57b1466177 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -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"
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
index a71dc65e07e..712ac5eb0e5 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
@@ -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>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
index e6a5fa4fa3e..6cfb795595d 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
@@ -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') }}
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue
index 65efc24e968..3232c0edf96 100644
--- a/app/assets/javascripts/reports/components/summary_row.vue
+++ b/app/assets/javascripts/reports/components/summary_row.vue
@@ -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>
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
index 8f632b87376..33f03fb5949 100644
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -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;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index ef50bcfc2f9..57ad9abef4b 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -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;
-}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 7164af213db..9085af0ff1c 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -109,3 +109,7 @@
.gl-transition-property-stroke {
transition-property: stroke;
}
+
+.gl-top-66vh {
+ top: 66vh;
+}
diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb
index 02e306bc500..6323bd01c58 100644
--- a/app/models/concerns/approvable_base.rb
+++ b/app/models/concerns/approvable_base.rb
@@ -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)
diff --git a/app/models/member.rb b/app/models/member.rb
index f2926d32d47..36f9741ce01 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -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')
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 9a916cd40ae..8c224dea88f 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -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?
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index feac98fc72f..e529ba6b486 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -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'
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
new file mode 100644
index 00000000000..53bfa3d979e
--- /dev/null
+++ b/app/models/namespace_setting.rb
@@ -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')
diff --git a/app/models/user.rb b/app/models/user.rb
index 36b9ed358ff..643b759e6f4 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index eb1b8d4fcc0..ce583095168 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -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
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 0b729981a93..610288c5e76 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -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?
diff --git a/app/services/merge_requests/remove_approval_service.rb b/app/services/merge_requests/remove_approval_service.rb
index 5bc44bdad00..3164d0b4069 100644
--- a/app/services/merge_requests/remove_approval_service.rb
+++ b/app/services/merge_requests/remove_approval_service.rb
@@ -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
diff --git a/changelogs/unreleased/196066-add-milestone-expired-info-be-2.yml b/changelogs/unreleased/196066-add-milestone-expired-info-be-2.yml
new file mode 100644
index 00000000000..6455216d16a
--- /dev/null
+++ b/changelogs/unreleased/196066-add-milestone-expired-info-be-2.yml
@@ -0,0 +1,5 @@
+---
+title: Add include_parent_milestones param to milestones API
+merge_request: 36944
+author:
+type: added
diff --git a/changelogs/unreleased/217392-update-workhorse-version.yml b/changelogs/unreleased/217392-update-workhorse-version.yml
new file mode 100644
index 00000000000..7e7cf835b7a
--- /dev/null
+++ b/changelogs/unreleased/217392-update-workhorse-version.yml
@@ -0,0 +1,5 @@
+---
+title: Update GITLAB_WORKHORSE_VERSION to 8.37.0
+merge_request: 36988
+author:
+type: other
diff --git a/changelogs/unreleased/224039-jira-issues-integration-doc.yml b/changelogs/unreleased/224039-jira-issues-integration-doc.yml
new file mode 100644
index 00000000000..0262543c574
--- /dev/null
+++ b/changelogs/unreleased/224039-jira-issues-integration-doc.yml
@@ -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
diff --git a/changelogs/unreleased/add-namespace-settings-table.yml b/changelogs/unreleased/add-namespace-settings-table.yml
new file mode 100644
index 00000000000..7e09b391efb
--- /dev/null
+++ b/changelogs/unreleased/add-namespace-settings-table.yml
@@ -0,0 +1,5 @@
+---
+title: Add namespace settings table
+merge_request: 36321
+author:
+type: added
diff --git a/changelogs/unreleased/dag-annotations-sticky.yml b/changelogs/unreleased/dag-annotations-sticky.yml
new file mode 100644
index 00000000000..763ee558e97
--- /dev/null
+++ b/changelogs/unreleased/dag-annotations-sticky.yml
@@ -0,0 +1,5 @@
+---
+title: Make DAG annotations stick
+merge_request: 37068
+author:
+type: changed
diff --git a/changelogs/unreleased/downstream-pipeline-ux.yml b/changelogs/unreleased/downstream-pipeline-ux.yml
new file mode 100644
index 00000000000..d8aec39bc41
--- /dev/null
+++ b/changelogs/unreleased/downstream-pipeline-ux.yml
@@ -0,0 +1,5 @@
+---
+title: Add correlation between trigger job and child pipeline
+merge_request: 36750
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-design-note-border-radius.yml b/changelogs/unreleased/fix-design-note-border-radius.yml
new file mode 100644
index 00000000000..af67e4ffce8
--- /dev/null
+++ b/changelogs/unreleased/fix-design-note-border-radius.yml
@@ -0,0 +1,5 @@
+---
+title: Fix background overflow when design note is selected
+merge_request: 36931
+author:
+type: fixed
diff --git a/changelogs/unreleased/remove-ff-in-count-users-by-group.yml b/changelogs/unreleased/remove-ff-in-count-users-by-group.yml
new file mode 100644
index 00000000000..0a1973fcf25
--- /dev/null
+++ b/changelogs/unreleased/remove-ff-in-count-users-by-group.yml
@@ -0,0 +1,5 @@
+---
+title: Remove optimized_count_users_by_group_id feature flag
+merge_request: 36953
+author:
+type: performance
diff --git a/changelogs/unreleased/xanf-expose-bitbucket-error.yml b/changelogs/unreleased/xanf-expose-bitbucket-error.yml
new file mode 100644
index 00000000000..571bc65bc30
--- /dev/null
+++ b/changelogs/unreleased/xanf-expose-bitbucket-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix displaying import errors from server
+merge_request: 37073
+author:
+type: fixed
diff --git a/db/migrate/20200703124823_create_namespace_settings.rb b/db/migrate/20200703124823_create_namespace_settings.rb
new file mode 100644
index 00000000000..907b9d2ca8c
--- /dev/null
+++ b/db/migrate/20200703124823_create_namespace_settings.rb
@@ -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
diff --git a/db/post_migrate/20200703125016_backfill_namespace_settings.rb b/db/post_migrate/20200703125016_backfill_namespace_settings.rb
new file mode 100644
index 00000000000..a7335e2d2b8
--- /dev/null
+++ b/db/post_migrate/20200703125016_backfill_namespace_settings.rb
@@ -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
diff --git a/db/structure.sql b/db/structure.sql
index c994885f5d1..d79e2f4eeba 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index b5702c7d6e0..b3a6e372b4c 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -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"
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index 9910f0651b8..54f8ca0d98b 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -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
diff --git a/doc/user/application_security/configuration/index.md b/doc/user/application_security/configuration/index.md
index 0f58b18734a..61e730ce09b 100644
--- a/doc/user/application_security/configuration/index.md
+++ b/doc/user/application_security/configuration/index.md
@@ -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.
diff --git a/doc/user/application_security/container_scanning/img/container_scanning_v13_1.png b/doc/user/application_security/container_scanning/img/container_scanning_v13_1.png
deleted file mode 100644
index 966296798ad..00000000000
--- a/doc/user/application_security/container_scanning/img/container_scanning_v13_1.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/container_scanning/img/container_scanning_v13_2.png b/doc/user/application_security/container_scanning/img/container_scanning_v13_2.png
new file mode 100644
index 00000000000..254ea1dcf5d
--- /dev/null
+++ b/doc/user/application_security/container_scanning/img/container_scanning_v13_2.png
Binary files differ
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index f6b0d661ba7..7bc8b62825c 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -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
diff --git a/doc/user/application_security/dast/img/dast_all_v13_1.png b/doc/user/application_security/dast/img/dast_all_v13_1.png
deleted file mode 100644
index 3c5f31412a7..00000000000
--- a/doc/user/application_security/dast/img/dast_all_v13_1.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/dast/img/dast_v13_2.png b/doc/user/application_security/dast/img/dast_v13_2.png
new file mode 100644
index 00000000000..bbf7944eb40
--- /dev/null
+++ b/doc/user/application_security/dast/img/dast_v13_2.png
Binary files differ
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 307ab037b0f..d68928d858b 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -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.
diff --git a/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_1.png b/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_1.png
deleted file mode 100644
index 3d3d28535a8..00000000000
--- a/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_1.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_2.png b/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_2.png
new file mode 100644
index 00000000000..28c4eb85b7c
--- /dev/null
+++ b/doc/user/application_security/dependency_scanning/img/dependency_scanning_v13_2.png
Binary files differ
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index 656f4045bc5..57b4fae3230 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -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:
diff --git a/doc/user/application_security/sast/img/sast_v13_1.png b/doc/user/application_security/sast/img/sast_v13_1.png
deleted file mode 100644
index 72ca8461659..00000000000
--- a/doc/user/application_security/sast/img/sast_v13_1.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/sast/img/sast_v13_2.png b/doc/user/application_security/sast/img/sast_v13_2.png
new file mode 100644
index 00000000000..5697ed9beb0
--- /dev/null
+++ b/doc/user/application_security/sast/img/sast_v13_2.png
Binary files differ
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index 8d09961a35d..2041f76475a 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -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:
diff --git a/doc/user/application_security/secret_detection/img/secret-detection-merge-request-ui.png b/doc/user/application_security/secret_detection/img/secret-detection-merge-request-ui.png
deleted file mode 100644
index 17893610f10..00000000000
--- a/doc/user/application_security/secret_detection/img/secret-detection-merge-request-ui.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/secret_detection/img/secret_detection_v13_2.png b/doc/user/application_security/secret_detection/img/secret_detection_v13_2.png
new file mode 100644
index 00000000000..4aa7dd83c8d
--- /dev/null
+++ b/doc/user/application_security/secret_detection/img/secret_detection_v13_2.png
Binary files differ
diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md
index 2c07c3c384d..ea635212c5d 100644
--- a/doc/user/application_security/secret_detection/index.md
+++ b/doc/user/application_security/secret_detection/index.md
@@ -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
diff --git a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v12_10.png b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v12_10.png
deleted file mode 100644
index 466552f746e..00000000000
--- a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v12_10.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_2.png b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_2.png
new file mode 100644
index 00000000000..e1edfcdd024
--- /dev/null
+++ b/doc/user/compliance/compliance_dashboard/img/compliance_dashboard_v13_2.png
Binary files differ
diff --git a/doc/user/compliance/compliance_dashboard/index.md b/doc/user/compliance/compliance_dashboard/index.md
index 08a26a45b17..e7db73e25d9 100644
--- a/doc/user/compliance/compliance_dashboard/index.md
+++ b/doc/user/compliance/compliance_dashboard/index.md
@@ -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
diff --git a/doc/user/group/iterations/index.md b/doc/user/group/iterations/index.md
index 656f461acc2..bc9d228011a 100644
--- a/doc/user/group/iterations/index.md
+++ b/doc/user/group/iterations/index.md
@@ -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:
diff --git a/doc/user/project/integrations/img/jira/open_jira_issues_list_v13.2.png b/doc/user/project/integrations/img/jira/open_jira_issues_list_v13.2.png
new file mode 100644
index 00000000000..aaf4f6e3f90
--- /dev/null
+++ b/doc/user/project/integrations/img/jira/open_jira_issues_list_v13.2.png
Binary files differ
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index 442f3229de2..c44eb2af4dc 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -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.
-
-![Jira service page](img/jira_service_page_v12_2.png)
+#### Disabling comments on Jira issues
-### Disabling comments on Jira issues
+You can continue to have GitLab cross-link a source commit/MR with a Jira issue while disabling the comment added to the issue.
-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.
diff --git a/lib/api/entities/merge_request_approvals.rb b/lib/api/entities/merge_request_approvals.rb
index d8a464cfeda..e3d58d687c4 100644
--- a/lib/api/entities/merge_request_approvals.rb
+++ b/lib/api/entities/merge_request_approvals.rb
@@ -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
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 3de7ae13d5e..4edf94c6350 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -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)
diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb
index 8ff885983bc..ec108ea8c83 100644
--- a/lib/api/milestone_responses.rb
+++ b/lib/api/milestone_responses.rb
@@ -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
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
index 2f8dd1085dc..ef74e8cb0c9 100644
--- a/lib/api/project_milestones.rb
+++ b/lib/api/project_milestones.rb
@@ -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
diff --git a/lib/gitlab/background_migration/backfill_namespace_settings.rb b/lib/gitlab/background_migration/backfill_namespace_settings.rb
new file mode 100644
index 00000000000..a391d5f4ebe
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_namespace_settings.rb
@@ -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
diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb
index d65c7887643..db799c094b2 100644
--- a/lib/gitlab/danger/helper.rb
+++ b/lib/gitlab/danger/helper.rb
@@ -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?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index de09be7c30b..fc2d7d475c1 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index a5f8c8b678e..1243609dc24 100644
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -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
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 7457e4c5023..40a220d57a7 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -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
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 008a3d6d181..2ca584ab8f6 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -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
diff --git a/spec/frontend/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index 1f2882a2532..fd6fbcbfce0 100644
--- a/spec/frontend/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -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,28 @@ 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,
+ [
+ { type: REQUEST_IMPORT, payload: importPayload.repo.id },
+ { type: RECEIVE_IMPORT_ERROR, payload: importPayload.repo.id },
+ ],
+ [],
+ );
+
+ 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,
@@ -143,6 +164,8 @@ describe('import_projects store actions', () => {
],
[],
);
+
+ expect(createFlash).toHaveBeenCalledWith(`Importing the project failed: ${ERROR_MESSAGE}`);
});
});
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
index d5d927eb63e..5f355ee8261 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
@@ -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>
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
new file mode 100644
index 00000000000..17ada722034
--- /dev/null
+++ b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
@@ -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>
+`;
diff --git a/spec/frontend/incidents_settings/components/alerts_form_spec.js b/spec/frontend/incidents_settings/components/alerts_form_spec.js
index 2a27347e40e..04832f31e58 100644
--- a/spec/frontend/incidents_settings/components/alerts_form_spec.js
+++ b/spec/frontend/incidents_settings/components/alerts_form_spec.js
@@ -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);
- findForm().trigger('submit');
- return waitForPromises().then(() => {
- expect(refreshCurrentPage).toHaveBeenCalled();
- });
- });
-
- it('should display a flah message on unsuccessful submit', () => {
- mock.onPatch().reply(400);
+ it('should call service `updateSettings` on submit', () => {
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,
+ }),
+ );
});
});
});
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
new file mode 100644
index 00000000000..58f9a318808
--- /dev/null
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -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);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
index c56b9ed2a69..47e2aecc108 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
@@ -6,7 +6,9 @@ describe('IncidentsSettingTabs', () => {
let wrapper;
beforeEach(() => {
- wrapper = shallowMount(IncidentsSettingTabs);
+ wrapper = shallowMount(IncidentsSettingTabs, {
+ provide: { glFeatures: { pagerdutyWebhook: true } },
+ });
});
afterEach(() => {
diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
new file mode 100644
index 00000000000..521094ad54c
--- /dev/null
+++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
@@ -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');
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index da777466e3e..2c5e7a1f6e9 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -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', () => {
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index cf78aa3ef71..133d5695afb 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -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([['']]);
+ });
});
});
diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
index 3e9c0814403..5756a666ff3 100644
--- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
+++ b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js
@@ -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: {
diff --git a/spec/frontend/pipelines/test_reports/test_summary_spec.js b/spec/frontend/pipelines/test_reports/test_summary_spec.js
index 8f041e46472..79be6c168cf 100644
--- a/spec/frontend/pipelines/test_reports/test_summary_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_summary_spec.js
@@ -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', () => {
diff --git a/spec/frontend/reports/components/summary_row_spec.js b/spec/frontend/reports/components/summary_row_spec.js
index cb0cc025e80..85c68ed069b 100644
--- a/spec/frontend/reports/components/summary_row_spec.js
+++ b/spec/frontend/reports/components/summary_row_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb
new file mode 100644
index 00000000000..43e76a2952e
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/backfill_namespace_settings_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
index 6d416c48530..ec2fd3cc4e0 100644
--- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb
@@ -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) }
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index bc8a34cd553..e73742b5911 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -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
diff --git a/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb b/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb
new file mode 100644
index 00000000000..7b84ef9e236
--- /dev/null
+++ b/spec/migrations/20200703125016_backfill_namespace_settings_spec.rb
@@ -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
diff --git a/spec/models/concerns/approvable_base_spec.rb b/spec/models/concerns/approvable_base_spec.rb
index e4aded1b8d0..8fda8bccf09 100644
--- a/spec/models/concerns/approvable_base_spec.rb
+++ b/spec/models/concerns/approvable_base_spec.rb
@@ -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
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 6cb35fd1be8..f155c240fb2 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -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
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 94091d5268e..9af620e70a5 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -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)
-
- 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
+ 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)
+
+ expect(described_class.count_users_by_group_id).to eq(group_1.id => 2,
+ group_2.id => 1)
end
describe '.of_ldap_type' do
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
new file mode 100644
index 00000000000..257d78dfa2c
--- /dev/null
+++ b/spec/models/namespace_setting_spec.rb
@@ -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
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 4d137f60f9a..ad4c8448745 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -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
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 69e15c22897..fa2e4b63648 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -3711,6 +3711,12 @@ RSpec.describe User do
expect(user.namespace).not_to be_nil
end
+
+ it 'creates the namespace setting' do
+ user.save!
+
+ expect(user.namespace.namespace_settings).to be_persisted
+ end
end
context 'for an existing user' do
diff --git a/spec/requests/api/group_import_spec.rb b/spec/requests/api/group_import_spec.rb
index cf7dc7ede51..ad67f737725 100644
--- a/spec/requests/api/group_import_spec.rb
+++ b/spec/requests/api/group_import_spec.rb
@@ -122,6 +122,7 @@ RSpec.describe API::GroupImport do
before do
allow_next_instance_of(Group) do |group|
allow(group).to receive(:persisted?).and_return(false)
+ allow(group).to receive(:save).and_return(false)
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index bbce5b4cfb6..23889912d7a 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -321,6 +321,26 @@ RSpec.describe API::Members do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
+
+ context 'adding project bot' do
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+
+ before do
+ unrelated_project = create(:project)
+ unrelated_project.add_maintainer(project_bot)
+ end
+
+ it 'returns 400' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/members", maintainer),
+ params: { user_id: project_bot.id, access_level: Member::DEVELOPER }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']['user_id']).to(
+ include('project bots cannot be added to other groups / projects'))
+ end.not_to change { project.members.count }
+ end
+ end
end
shared_examples 'PUT /:source_type/:id/members/:user_id' do |source_type|
@@ -461,8 +481,34 @@ RSpec.describe API::Members do
end
end
- it_behaves_like 'POST /:source_type/:id/members', 'project' do
- let(:source) { project }
+ describe 'POST /projects/:id/members' do
+ it_behaves_like 'POST /:source_type/:id/members', 'project' do
+ let(:source) { project }
+ end
+
+ context 'adding owner to project' do
+ it 'returns 403' do
+ expect do
+ post api("/projects/#{project.id}/members", maintainer),
+ params: { user_id: stranger.id, access_level: Member::OWNER }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end.not_to change { project.members.count }
+ end
+ end
+
+ context 'remove bot from project' do
+ it 'returns a 403 forbidden' do
+ project_bot = create(:user, :project_bot)
+ create(:project_member, project: project, user: project_bot)
+
+ expect do
+ delete api("/projects/#{project.id}/members/#{project_bot.id}", maintainer)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end.not_to change { project.members.count }
+ end
+ end
end
it_behaves_like 'POST /:source_type/:id/members', 'group' do
@@ -484,28 +530,4 @@ RSpec.describe API::Members do
it_behaves_like 'DELETE /:source_type/:id/members/:user_id', 'group' do
let(:source) { group }
end
-
- context 'Adding owner to project' do
- it 'returns 403' do
- expect do
- post api("/projects/#{project.id}/members", maintainer),
- params: { user_id: stranger.id, access_level: Member::OWNER }
-
- expect(response).to have_gitlab_http_status(:bad_request)
- end.to change { project.members.count }.by(0)
- end
- end
-
- context 'remove bot from project' do
- it 'returns a 403 forbidden' do
- project_bot = create(:user, :project_bot)
- create(:project_member, project: project, user: project_bot)
-
- expect do
- delete api("/projects/#{project.id}/members/#{project_bot.id}", maintainer)
-
- expect(response).to have_gitlab_http_status(:forbidden)
- end.to change { project.members.count }.by(0)
- end
- end
end
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index b238949ce47..507e9fa6710 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
RSpec.describe API::ProjectMilestones do
- let(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace ) }
- let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
- let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: user.namespace ) }
+ let_it_be(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
+ let_it_be(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
before do
project.add_developer(user)
@@ -16,6 +16,65 @@ RSpec.describe API::ProjectMilestones do
let(:route) { "/projects/#{project.id}/milestones" }
end
+ describe 'GET /projects/:id/milestones' do
+ context 'when include_parent_milestones is true' do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:child_group) { create(:group, :public, parent: group) }
+ let_it_be(:child_project) { create(:project, group: child_group) }
+ let_it_be(:project_milestone) { create(:milestone, project: child_project) }
+ let_it_be(:group_milestone) { create(:milestone, group: group) }
+ let_it_be(:child_group_milestone) { create(:milestone, group: child_group) }
+
+ before do
+ child_project.add_developer(user)
+ end
+
+ it 'includes parent groups milestones' do
+ milestones = [child_group_milestone, group_milestone, project_milestone]
+
+ get api("/projects/#{child_project.id}/milestones", user),
+ params: { include_parent_milestones: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.size).to eq(3)
+ expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id))
+ end
+
+ context 'when user has no access to an ancestor group' do
+ before do
+ [child_group, group].each do |group|
+ group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ it 'does not show ancestor group milestones' do
+ milestones = [child_group_milestone, project_milestone]
+
+ get api("/projects/#{child_project.id}/milestones", user),
+ params: { include_parent_milestones: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id))
+ end
+ end
+
+ context 'when filtering by iids' do
+ it 'does not filter by iids' do
+ milestones = [child_group_milestone, group_milestone, project_milestone]
+
+ get api("/projects/#{child_project.id}/milestones", user),
+ params: { include_parent_milestones: true, iids: [group_milestone.iid] }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.size).to eq(3)
+
+ expect(json_response.map { |entry| entry["id"] }).to eq(milestones.map(&:id))
+ end
+ end
+ end
+ end
+
describe 'DELETE /projects/:id/milestones/:milestone_id' do
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index b2e9db8c9de..fc877f45a39 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -129,4 +129,13 @@ RSpec.describe Groups::CreateService, '#execute' do
expect { subject }.to change { ChatTeam.count }.from(0).to(1)
end
end
+
+ describe 'creating a setting record' do
+ let(:service) { described_class.new(user, group_params) }
+
+ it 'create the settings record connected to the group' do
+ group = subject
+ expect(group.namespace_settings).to be_persisted
+ end
+ end
end