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:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-06 21:09:07 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-06 21:09:07 +0300
commitf3db01da507f86cfed412c7d337e3747744cc914 (patch)
tree3862e3ca223038c1390e2d19708ebeeecb040e00
parenta268b09416c8dc3da3af38933028fa26375b88e0 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue44
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue94
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue94
-rw-r--r--app/assets/javascripts/alerts_settings/constants.js2
-rw-r--r--app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql10
-rw-r--r--app/assets/javascripts/alerts_settings/utils/cache_updates.js84
-rw-r--r--app/assets/javascripts/alerts_settings/utils/error_messages.js9
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue8
-rw-r--r--app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue6
-rw-r--r--app/assets/javascripts/registry/explorer/pages/details.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue14
-rw-r--r--app/assets/stylesheets/framework/broadcast_messages.scss8
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/finders/issuable_finder.rb6
-rw-r--r--app/finders/merge_requests_finder.rb7
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/models/merge_request.rb13
-rw-r--r--app/models/resource_timebox_event.rb4
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/projects/merge_requests/_nav_btns.html.haml8
-rw-r--r--app/views/projects/settings/operations/_error_tracking.html.haml2
-rw-r--r--app/views/shared/_broadcast_message.html.haml4
-rw-r--r--changelogs/unreleased/251136-delete-selected-button-in-container-registry-is-not-visible-on-nar.yml5
-rw-r--r--changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml5
-rw-r--r--changelogs/unreleased/bvl-handle-invalid-headers.yml5
-rw-r--r--changelogs/unreleased/nicolasdular-fix-bm-close-icon.yml5
-rw-r--r--changelogs/unreleased/pl-fix-operations-settings-wo-pipelines.yml5
-rw-r--r--config/feature_flags/development/export_merge_requests_as_csv.yml7
-rw-r--r--doc/administration/audit_reports.md12
-rw-r--r--doc/administration/geo/disaster_recovery/index.md6
-rw-r--r--doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md3
-rw-r--r--doc/administration/geo/index.md3
-rw-r--r--doc/administration/geo/replication/version_specific_updates.md7
-rw-r--r--doc/api/releases/index.md2
-rw-r--r--doc/api/releases/links.md20
-rw-r--r--doc/ci/docker/using_docker_build.md2
-rw-r--r--doc/ci/yaml/README.md50
-rw-r--r--doc/gitlab-basics/command-line-commands.md12
-rw-r--r--doc/user/project/merge_requests/csv_export.md38
-rw-r--r--doc/user/project/settings/import_export.md8
-rw-r--r--lib/gitlab/graphql/loaders/batch_model_loader.rb11
-rw-r--r--lib/gitlab/middleware/handle_malformed_strings.rb23
-rw-r--r--lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb25
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb16
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb19
-rw-r--r--spec/features/file_uploads/multipart_invalid_uploads_spec.rb6
-rw-r--r--spec/features/merge_requests/user_exports_as_csv_spec.rb31
-rw-r--r--spec/finders/merge_requests_finder_spec.rb81
-rw-r--r--spec/frontend/alerts_settings/alerts_settings_form_new_spec.js24
-rw-r--r--spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js87
-rw-r--r--spec/frontend/alerts_settings/mocks/apollo_mock.js97
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js4
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_spec.js24
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js4
-rw-r--r--spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb23
-rw-r--r--spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb44
-rw-r--r--spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb30
-rw-r--r--spec/requests/user_sends_malformed_strings_spec.rb10
-rw-r--r--spec/views/projects/settings/operations/show.html.haml_spec.rb2
62 files changed, 736 insertions, 468 deletions
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
index 5ecb2dd3e58..f24c52f61da 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue
@@ -4,12 +4,15 @@ import {
GlButton,
GlIcon,
GlLoadingIcon,
+ GlModal,
+ GlModalDirective,
GlTable,
GlTooltipDirective,
+ GlSprintf,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
-import { trackAlertIntegrationsViewsOptions } from '../constants';
+import { trackAlertIntegrationsViewsOptions, integrationToDeleteDefault } from '../constants';
export const i18n = {
title: s__('AlertsIntegrations|Current integrations'),
@@ -36,10 +39,13 @@ export default {
GlButton,
GlIcon,
GlLoadingIcon,
+ GlModal,
GlTable,
+ GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
+ GlModal: GlModalDirective,
},
props: {
integrations: {
@@ -71,6 +77,11 @@ export default {
label: __('Actions'),
},
],
+ data() {
+ return {
+ integrationToDelete: integrationToDeleteDefault,
+ };
+ },
computed: {
tbodyTrClass() {
return {
@@ -86,6 +97,14 @@ export default {
const { category, action } = trackAlertIntegrationsViewsOptions;
Tracking.event(category, action);
},
+ intergrationToDelete({ name, id }) {
+ this.integrationToDelete.id = id;
+ this.integrationToDelete.name = name;
+ },
+ deleteIntergration() {
+ this.$emit('delete-integration', { id: this.integrationToDelete.id });
+ this.integrationToDelete = { ...integrationToDeleteDefault };
+ },
},
};
</script>
@@ -127,7 +146,11 @@ export default {
<template #cell(actions)="{ item }">
<gl-button-group>
<gl-button icon="pencil" @click="$emit('edit-integration', { id: item.id })" />
- <gl-button icon="remove" @click="$emit('delete-integration', { id: item.id })" />
+ <gl-button
+ v-gl-modal.deleteIntegration
+ icon="remove"
+ @click="intergrationToDelete(item)"
+ />
</gl-button-group>
</template>
@@ -143,5 +166,22 @@ export default {
</div>
</template>
</gl-table>
+ <gl-modal
+ modal-id="deleteIntegration"
+ :title="__('Are you sure?')"
+ :ok-title="s__('AlertSettings|Delete integration')"
+ ok-variant="danger"
+ @ok="deleteIntergration"
+ >
+ <gl-sprintf
+ :message="
+ s__(
+ 'AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone.',
+ )
+ "
+ >
+ <template #integrationName>{{ integrationToDelete.name }}</template>
+ </gl-sprintf>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue
index 059623ba11c..946da8ef34c 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue
@@ -22,14 +22,12 @@ import {
JSON_VALIDATE_DELAY,
targetPrometheusUrlPlaceholder,
typeSet,
- defaultFormState,
} from '../constants';
export default {
targetPrometheusUrlPlaceholder,
JSON_VALIDATE_DELAY,
typeSet,
- defaultFormState,
i18n: {
integrationFormSteps: {
step1: {
@@ -113,14 +111,18 @@ export default {
data() {
return {
selectedIntegration: integrationTypesNew[0].value,
- active: false,
options: integrationTypesNew,
+ active: false,
formVisible: false,
+ integrationTestPayload: {
+ json: null,
+ error: null,
+ },
};
},
computed: {
jsonIsValid() {
- return this.integrationForm.integrationTestPayload.error === null;
+ return this.integrationTestPayload.error === null;
},
selectedIntegrationType() {
switch (this.selectedIntegration) {
@@ -129,43 +131,42 @@ export default {
case this.$options.typeSet.prometheus:
return this.prometheus;
default:
- return this.defaultFormState;
+ return {};
}
},
integrationForm() {
return {
name: this.currentIntegration?.name || '',
- integrationTestPayload: {
- json: null,
- error: null,
- },
active: this.currentIntegration?.active || false,
- token: this.currentIntegration?.token || '',
- url: this.currentIntegration?.url || '',
+ token: this.currentIntegration?.token || this.selectedIntegrationType.token,
+ url: this.currentIntegration?.url || this.selectedIntegrationType.url,
apiUrl: this.currentIntegration?.apiUrl || '',
};
},
},
watch: {
currentIntegration(val) {
+ if (val === null) {
+ return this.reset();
+ }
this.selectedIntegration = val.type;
this.active = val.active;
- this.onIntegrationTypeSelect();
+ return this.integrationTypeSelect();
},
},
methods: {
- onIntegrationTypeSelect() {
+ integrationTypeSelect() {
if (this.selectedIntegration === integrationTypesNew[0].value) {
this.formVisible = false;
} else {
this.formVisible = true;
}
},
- onSubmitWithTestPayload() {
+ submitWithTestPayload() {
// TODO: Test payload before saving via GraphQL
- this.onSubmit();
+ this.submit();
},
- onSubmit() {
+ submit() {
const { name, apiUrl } = this.integrationForm;
const variables =
this.selectedIntegration === this.$options.typeSet.http
@@ -179,27 +180,45 @@ export default {
return this.$emit('create-new-integration', integrationPayload);
},
- onReset() {
- this.integrationForm = this.defaultFormState;
+ reset() {
this.selectedIntegration = integrationTypesNew[0].value;
- this.onIntegrationTypeSelect();
+ this.integrationTypeSelect();
+
+ if (this.currentIntegration) {
+ return this.$emit('clear-current-integration');
+ }
+
+ return this.resetFormValues();
+ },
+ resetFormValues() {
+ this.integrationForm.name = '';
+ this.integrationForm.apiUrl = '';
+ this.integrationTestPayload = {
+ json: null,
+ error: null,
+ };
+ this.active = false;
},
- onResetAuthKey() {
+ resetAuthKey() {
+ if (!this.currentIntegration) {
+ return;
+ }
+
this.$emit('reset-token', {
type: this.selectedIntegration,
variables: { id: this.currentIntegration.id },
});
},
validateJson() {
- this.integrationForm.integrationTestPayload.error = null;
- if (this.integrationForm.integrationTestPayload.json === '') {
+ this.integrationTestPayload.error = null;
+ if (this.integrationTestPayload.json === '') {
return;
}
try {
- JSON.parse(this.integrationForm.integrationTestPayload.json);
+ JSON.parse(this.integrationTestPayload.json);
} catch (e) {
- this.integrationForm.integrationTestPayload.error = JSON.stringify(e.message);
+ this.integrationTestPayload.error = JSON.stringify(e.message);
}
},
},
@@ -207,7 +226,7 @@ export default {
</script>
<template>
- <gl-form class="gl-mt-6" @submit.prevent="onSubmit" @reset.prevent="onReset">
+ <gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset">
<h5 class="gl-font-lg gl-my-5">{{ s__('AlertSettings|Add new integrations') }}</h5>
<gl-form-group
@@ -217,8 +236,9 @@ export default {
>
<gl-form-select
v-model="selectedIntegration"
+ :disabled="currentIntegration !== null"
:options="options"
- @change="onIntegrationTypeSelect"
+ @change="integrationTypeSelect"
/>
<alert-settings-form-help-block
@@ -279,7 +299,11 @@ export default {
<gl-form-input-group id="url" readonly :value="integrationForm.url">
<template #append>
- <clipboard-button :text="integrationForm.url" :title="__('Copy')" class="gl-m-0!" />
+ <clipboard-button
+ :text="integrationForm.url || ''"
+ :title="__('Copy')"
+ class="gl-m-0!"
+ />
</template>
</gl-form-input-group>
</div>
@@ -296,7 +320,11 @@ export default {
:value="integrationForm.token"
>
<template #append>
- <clipboard-button :text="integrationForm.token" :title="__('Copy')" class="gl-m-0!" />
+ <clipboard-button
+ :text="integrationForm.token || ''"
+ :title="__('Copy')"
+ class="gl-m-0!"
+ />
</template>
</gl-form-input-group>
@@ -308,7 +336,7 @@ export default {
:title="$options.i18n.integrationFormSteps.step3.reset"
:ok-title="$options.i18n.integrationFormSteps.step3.reset"
ok-variant="danger"
- @ok="onResetAuthKey"
+ @ok="resetAuthKey"
>
{{ $options.i18n.integrationFormSteps.restKeyInfo.label }}
</gl-modal>
@@ -318,7 +346,7 @@ export default {
id="test-integration"
:label="$options.i18n.integrationFormSteps.step4.label"
label-for="test-integration"
- :invalid-feedback="integrationForm.integrationTestPayload.error"
+ :invalid-feedback="integrationTestPayload.error"
>
<alert-settings-form-help-block
:message="$options.i18n.integrationFormSteps.step4.help"
@@ -327,8 +355,8 @@ export default {
<gl-form-textarea
id="test-integration"
- v-model.trim="integrationForm.integrationTestPayload.json"
- :disabled="!integrationForm.active"
+ v-model.trim="integrationTestPayload.json"
+ :disabled="!active"
:state="jsonIsValid"
:placeholder="$options.i18n.integrationFormSteps.step4.placeholder"
class="gl-my-4"
@@ -354,7 +382,7 @@ export default {
category="secondary"
variant="success"
class="gl-mr-1 js-no-auto-disable"
- @click="onSubmitWithTestPayload"
+ @click="submitWithTestPayload"
>{{ s__('AlertSettings|Save and test payload') }}</gl-button
>
<gl-button
diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
index 0a59a5981ef..e9e7b1407bc 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -1,5 +1,4 @@
<script>
-import produce from 'immer';
import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql';
@@ -9,12 +8,17 @@ import createHttpIntegrationMutation from '../graphql/mutations/create_http_inte
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
import updateHttpIntegrationMutation from '../graphql/mutations/update_http_integration.mutation.graphql';
import updatePrometheusIntegrationMutation from '../graphql/mutations/update_prometheus_integration.mutation.graphql';
+import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql';
import IntegrationsList from './alerts_integrations_list.vue';
import SettingsFormOld from './alerts_settings_form_old.vue';
import SettingsFormNew from './alerts_settings_form_new.vue';
import { typeSet } from '../constants';
+import {
+ updateStoreAfterIntegrationDelete,
+ updateStoreAfterIntegrationAdd,
+} from '../utils/cache_updates';
export default {
typeSet,
@@ -22,6 +26,7 @@ export default {
changesSaved: s__(
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
),
+ integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'),
},
components: {
IntegrationsList,
@@ -89,6 +94,8 @@ export default {
},
methods: {
createNewIntegration({ type, variables }) {
+ const { projectPath } = this;
+
this.isUpdating = true;
this.$apollo
.mutate({
@@ -98,9 +105,11 @@ export default {
: createPrometheusIntegrationMutation,
variables: {
...variables,
- projectPath: this.projectPath,
+ projectPath,
+ },
+ update(store, { data }) {
+ updateStoreAfterIntegrationAdd(store, getIntegrationsQuery, data, { projectPath });
},
- update: this.updateIntegrations,
})
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
@@ -119,41 +128,6 @@ export default {
this.isUpdating = false;
});
},
- updateIntegrations(
- store,
- {
- data: { httpIntegrationCreate, prometheusIntegrationCreate },
- },
- ) {
- const integration =
- httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
- if (!integration) {
- return;
- }
-
- const sourceData = store.readQuery({
- query: getIntegrationsQuery,
- variables: {
- projectPath: this.projectPath,
- },
- });
-
- const data = produce(sourceData, draftData => {
- // eslint-disable-next-line no-param-reassign
- draftData.project.alertManagementIntegrations.nodes = [
- integration,
- ...draftData.project.alertManagementIntegrations.nodes,
- ];
- });
-
- store.writeQuery({
- query: getIntegrationsQuery,
- variables: {
- projectPath: this.projectPath,
- },
- data,
- });
- },
updateIntegration({ type, variables }) {
this.isUpdating = true;
this.$apollo
@@ -201,6 +175,12 @@ export default {
if (error) {
return createFlash({ message: error });
}
+
+ const integration =
+ httpIntegrationResetToken?.integration ||
+ prometheusIntegrationResetToken?.integration;
+ this.currentIntegration = integration;
+
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
@@ -217,8 +197,41 @@ export default {
editIntegration({ id }) {
this.currentIntegration = this.integrations.list.find(integration => integration.id === id);
},
- deleteIntegration() {
- // TODO, handle delete via GraphQL
+ deleteIntegration({ id }) {
+ const { projectPath } = this;
+
+ this.isUpdating = true;
+ this.$apollo
+ .mutate({
+ mutation: destroyHttpIntegrationMutation,
+ variables: {
+ id,
+ },
+ update(store, { data }) {
+ updateStoreAfterIntegrationDelete(store, getIntegrationsQuery, data, { projectPath });
+ },
+ })
+ .then(({ data: { httpIntegrationDestroy } = {} } = {}) => {
+ const error = httpIntegrationDestroy?.errors[0];
+ if (error) {
+ return createFlash({ message: error });
+ }
+ this.currentIntegration = null;
+ return createFlash({
+ message: this.$options.i18n.integrationRemoved,
+ type: FLASH_TYPES.SUCCESS,
+ });
+ })
+ .catch(err => {
+ this.errored = true;
+ createFlash({ message: err });
+ })
+ .finally(() => {
+ this.isUpdating = false;
+ });
+ },
+ clearCurrentIntegration() {
+ this.currentIntegration = null;
},
},
};
@@ -239,6 +252,7 @@ export default {
@create-new-integration="createNewIntegration"
@update-integration="updateIntegration"
@reset-token="resetToken"
+ @clear-current-integration="clearCurrentIntegration"
/>
<settings-form-old v-else />
</div>
diff --git a/app/assets/javascripts/alerts_settings/constants.js b/app/assets/javascripts/alerts_settings/constants.js
index 4ab8d215572..9cf2f356e0a 100644
--- a/app/assets/javascripts/alerts_settings/constants.js
+++ b/app/assets/javascripts/alerts_settings/constants.js
@@ -66,6 +66,8 @@ export const defaultFormState = {
integrationTestPayload: { json: null, error: null },
};
+export const integrationToDeleteDefault = { id: null, name: '' };
+
export const JSON_VALIDATE_DELAY = 250;
export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/';
diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql
new file mode 100644
index 00000000000..0a49c140e6a
--- /dev/null
+++ b/app/assets/javascripts/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql
@@ -0,0 +1,10 @@
+#import "../fragments/integration_item.fragment.graphql"
+
+mutation destroyHttpIntegration($id: ID!) {
+ httpIntegrationDestroy(input: { id: $id }) {
+ errors
+ integration {
+ ...IntegrationItem
+ }
+ }
+}
diff --git a/app/assets/javascripts/alerts_settings/utils/cache_updates.js b/app/assets/javascripts/alerts_settings/utils/cache_updates.js
new file mode 100644
index 00000000000..18054b29fe9
--- /dev/null
+++ b/app/assets/javascripts/alerts_settings/utils/cache_updates.js
@@ -0,0 +1,84 @@
+import produce from 'immer';
+import createFlash from '~/flash';
+
+import { DELETE_INTEGRATION_ERROR, ADD_INTEGRATION_ERROR } from './error_messages';
+
+const deleteIntegrationFromStore = (store, query, { httpIntegrationDestroy }, variables) => {
+ const integration = httpIntegrationDestroy?.integration;
+ if (!integration) {
+ return;
+ }
+
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.alertManagementIntegrations.nodes = draftData.project.alertManagementIntegrations.nodes.filter(
+ ({ id }) => id !== integration.id,
+ );
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+};
+
+const addIntegrationToStore = (
+ store,
+ query,
+ { httpIntegrationCreate, prometheusIntegrationCreate },
+ variables,
+) => {
+ const integration =
+ httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
+ if (!integration) {
+ return;
+ }
+
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, draftData => {
+ // eslint-disable-next-line no-param-reassign
+ draftData.project.alertManagementIntegrations.nodes = [
+ integration,
+ ...draftData.project.alertManagementIntegrations.nodes,
+ ];
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+};
+
+const onError = (data, message) => {
+ createFlash({ message });
+ throw new Error(data.errors);
+};
+
+export const hasErrors = ({ errors = [] }) => errors?.length;
+
+export const updateStoreAfterIntegrationDelete = (store, query, data, variables) => {
+ if (hasErrors(data)) {
+ onError(data, DELETE_INTEGRATION_ERROR);
+ } else {
+ deleteIntegrationFromStore(store, query, data, variables);
+ }
+};
+
+export const updateStoreAfterIntegrationAdd = (store, query, data, variables) => {
+ if (hasErrors(data)) {
+ onError(data, ADD_INTEGRATION_ERROR);
+ } else {
+ addIntegrationToStore(store, query, data, variables);
+ }
+};
diff --git a/app/assets/javascripts/alerts_settings/utils/error_messages.js b/app/assets/javascripts/alerts_settings/utils/error_messages.js
new file mode 100644
index 00000000000..2e6058fc81a
--- /dev/null
+++ b/app/assets/javascripts/alerts_settings/utils/error_messages.js
@@ -0,0 +1,9 @@
+import { s__ } from '~/locale';
+
+export const DELETE_INTEGRATION_ERROR = s__(
+ 'AlertsIntegrations|The integration could not be deleted. Please try again.',
+);
+
+export const ADD_INTEGRATION_ERROR = s__(
+ 'AlertsIntegrations|The integration could not be added. Please try again.',
+);
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
index 328026d0953..2844b4ffde3 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue
@@ -14,9 +14,9 @@ export default {
required: false,
default: () => [],
},
- isDesktop: {
+ isMobile: {
type: Boolean,
- default: false,
+ default: true,
required: false,
},
},
@@ -34,7 +34,7 @@ export default {
return this.tags.some(tag => this.selectedItems[tag.name]);
},
showMultiDeleteButton() {
- return this.tags.some(tag => tag.destroy_path) && this.isDesktop;
+ return this.tags.some(tag => tag.destroy_path) && !this.isMobile;
},
},
methods: {
@@ -68,7 +68,7 @@ export default {
:tag="tag"
:first="index === 0"
:selected="selectedItems[tag.name]"
- :is-desktop="isDesktop"
+ :is-mobile="isMobile"
@select="updateSelectedItems(tag.name)"
@delete="$emit('delete', { [tag.name]: true })"
/>
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
index 0f6297ca406..2edeac1144f 100644
--- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue
@@ -40,9 +40,9 @@ export default {
type: Object,
required: true,
},
- isDesktop: {
+ isMobile: {
type: Boolean,
- default: false,
+ default: true,
required: false,
},
selected: {
@@ -69,7 +69,7 @@ export default {
return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : '';
},
mobileClasses() {
- return this.isDesktop ? '' : 'mw-s';
+ return this.isMobile ? 'mw-s' : '';
},
shortDigest() {
// remove sha256: from the string, and show only the first 7 char
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue
index d2fb695dbfa..e110c9c9c70 100644
--- a/app/assets/javascripts/registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/registry/explorer/pages/details.vue
@@ -37,7 +37,7 @@ export default {
data() {
return {
itemsToBeDeleted: [],
- isDesktop: true,
+ isMobile: false,
deleteAlertType: null,
dismissPartialCleanupWarning: false,
};
@@ -110,7 +110,7 @@ export default {
}
},
handleResize() {
- this.isDesktop = GlBreakpointInstance.isDesktop();
+ this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs';
},
},
};
@@ -137,7 +137,7 @@ export default {
<tags-loader v-if="isLoading" />
<template v-else>
<empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
- <tags-list v-else :tags="tags" :is-desktop="isDesktop" @delete="deleteTags" />
+ <tags-list v-else :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
</template>
<gl-pagination
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
index f17e409d996..b6722de5277 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/commit_message_dropdown.vue
@@ -1,10 +1,10 @@
<script>
-import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
components: {
- GlDeprecatedDropdown,
- GlDeprecatedDropdownItem,
+ GlDropdown,
+ GlDropdownItem,
},
props: {
commits: {
@@ -18,20 +18,20 @@ export default {
<template>
<div>
- <gl-deprecated-dropdown
+ <gl-dropdown
right
text="Use an existing commit message"
variant="link"
class="mr-commit-dropdown"
>
- <gl-deprecated-dropdown-item
+ <gl-dropdown-item
v-for="commit in commits"
:key="commit.short_id"
class="text-nowrap text-truncate"
@click="$emit('input', commit.message)"
>
<span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }}
- </gl-deprecated-dropdown-item>
- </gl-deprecated-dropdown>
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/stylesheets/framework/broadcast_messages.scss b/app/assets/stylesheets/framework/broadcast_messages.scss
index c1647c16c77..b8934d2797a 100644
--- a/app/assets/stylesheets/framework/broadcast_messages.scss
+++ b/app/assets/stylesheets/framework/broadcast_messages.scss
@@ -15,10 +15,6 @@
.broadcast-banner-message {
text-align: center;
-
- .broadcast-message-dismiss {
- color: inherit;
- }
}
.broadcast-notification-message {
@@ -36,10 +32,6 @@
&.preview {
position: static;
}
-
- .broadcast-message-dismiss {
- color: $gray-700;
- }
}
.toggle-colors {
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 3f5f3b6e9df..0d7af57328a 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -150,7 +150,7 @@ module IssuableCollections
common_attributes + [:project, project: :namespace]
when 'MergeRequest'
common_attributes + [
- :target_project, :latest_merge_request_diff, :approvals, :approved_by_users, :reviewers,
+ :target_project, :latest_merge_request_diff, :approvals, :approved_by_users,
source_project: :route, head_pipeline: :project, target_project: :namespace
]
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index d42e97fb2d8..1d28c828464 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -318,8 +318,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def export_csv
- return render_404 unless Feature.enabled?(:export_merge_requests_as_csv, project, default_enabled: true)
-
IssuableExportCsvWorker.perform_async(:merge_request, current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
index_path = project_merge_requests_path(project)
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index ebbd5d739e6..d431c3e3699 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -406,7 +406,7 @@ class IssuableFinder
elsif params.filter_by_any_assignee?
items.assigned
elsif params.assignee
- items_assigned_to(items, params.assignee)
+ items.assigned_to(params.assignee)
elsif params.assignee_id? || params.assignee_username? # assignee not found
items.none
else
@@ -414,10 +414,6 @@ class IssuableFinder
end
end
- def items_assigned_to(items, user)
- items.assigned_to(user)
- end
-
def by_negated_assignee(items)
# We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB"
if not_params.assignees.present?
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 74cc90797b1..1f847b09752 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -164,13 +164,6 @@ class MergeRequestsFinder < IssuableFinder
end
# rubocop: enable CodeReuse/Finder
- # rubocop: disable CodeReuse/ActiveRecord
- def items_assigned_to(items, user)
- assignee_or_reviewer = MergeRequest.from_union([super, items.reviewer_assigned_to(user)])
- items.where(id: assignee_or_reviewer)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def by_deployments(items)
env = params[:environment]
before = params[:deployed_before]
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 9d1f685960f..a3919008109 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -302,7 +302,7 @@ module ProjectsHelper
end
def settings_operations_available?
- can?(current_user, :read_environment, @project)
+ !@project.archived? && can?(current_user, :admin_operations, @project)
end
def error_tracking_setting_project_json
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a0e0375fbe9..bf7a73f24e4 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -303,19 +303,6 @@ class MergeRequest < ApplicationRecord
includes(:metrics)
end
- scope :reviewer_assigned_to, ->(user) do
- mr_reviewers_table = MergeRequestReviewer.arel_table
-
- inner_sql = mr_reviewers_table
- .project(Arel::Nodes::True.new)
- .where(
- mr_reviewers_table[:merge_request_id].eq(MergeRequest.arel_table[:id])
- .and(mr_reviewers_table[:user_id].eq(user.id))
- ).exists
-
- where(inner_sql)
- end
-
after_save :keep_around_commit, unless: :importing?
alias_attribute :project, :target_project
diff --git a/app/models/resource_timebox_event.rb b/app/models/resource_timebox_event.rb
index dbb2b428c7b..ac164783945 100644
--- a/app/models/resource_timebox_event.rb
+++ b/app/models/resource_timebox_event.rb
@@ -29,10 +29,10 @@ class ResourceTimeboxEvent < ResourceEvent
case self
when ResourceMilestoneEvent
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user)
- when ResourceIterationEvent
- Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_iteration_changed_action(author: user)
else
# no-op
end
end
end
+
+ResourceTimeboxEvent.prepend_if_ee('EE::ResourceTimeboxEvent')
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index d1168437c5d..bff8c861f1f 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -423,7 +423,7 @@
= link_to project_settings_ci_cd_path(@project), title: _('CI / CD') do
%span
= _('CI / CD')
- - if !@project.archived? && settings_operations_available?
+ - if settings_operations_available?
= nav_link(controller: [:operations]) do
= link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
= _('Operations')
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
index 9d367caa390..473490c6c35 100644
--- a/app/views/projects/merge_requests/_nav_btns.html.haml
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -1,6 +1,5 @@
-- if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
- .btn-group
- = render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
+.btn-group
+ = render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
- if @can_bulk_update
= button_tag "Edit merge requests", class: "gl-button btn gl-mr-3 js-bulk-update-toggle"
@@ -8,5 +7,4 @@
= link_to new_merge_request_path, class: "gl-button btn btn-success", title: "New merge request" do
New merge request
- - if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
- = render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'
+ = render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'
diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml
index 62b344b38f1..6ab8beff99f 100644
--- a/app/views/projects/settings/operations/_error_tracking.html.haml
+++ b/app/views/projects/settings/operations/_error_tracking.html.haml
@@ -1,4 +1,4 @@
-- return unless can?(current_user, :read_environment, @project)
+- return unless can?(current_user, :admin_operations, @project)
- setting = error_tracking_setting
diff --git a/app/views/shared/_broadcast_message.html.haml b/app/views/shared/_broadcast_message.html.haml
index 7be11b0fb81..7aaae3a88f3 100644
--- a/app/views/shared/_broadcast_message.html.haml
+++ b/app/views/shared/_broadcast_message.html.haml
@@ -8,5 +8,5 @@
= render_broadcast_message(message)
.gl-flex-grow-1.gl-flex-basis-0.gl-text-right
- if (message.notification? || message.dismissable?) && opts[:preview].blank?
- %button.broadcast-message-dismiss.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
- = sprite_icon('close', size: 16, css_class: 'gl-icon gl-text-white gl-mx-3!')
+ %button.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
+ = sprite_icon('close', size: 16, css_class: "gl-icon gl-mx-3! #{is_banner ? 'gl-text-white' : 'gl-text-gray-700'}")
diff --git a/changelogs/unreleased/251136-delete-selected-button-in-container-registry-is-not-visible-on-nar.yml b/changelogs/unreleased/251136-delete-selected-button-in-container-registry-is-not-visible-on-nar.yml
new file mode 100644
index 00000000000..ccf44a02ca9
--- /dev/null
+++ b/changelogs/unreleased/251136-delete-selected-button-in-container-registry-is-not-visible-on-nar.yml
@@ -0,0 +1,5 @@
+---
+title: 'container registry: show delete selected button on medium viewports'
+merge_request: 46699
+author:
+type: fixed
diff --git a/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml
new file mode 100644
index 00000000000..db712295b7b
--- /dev/null
+++ b/changelogs/unreleased/Replace-GlDeprecatedDropdown-with-GlDropdown-in-app-assets-javascripts-vu.yml
@@ -0,0 +1,5 @@
+---
+title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/vue_merge_request_widget
+merge_request: 41429
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/bvl-handle-invalid-headers.yml b/changelogs/unreleased/bvl-handle-invalid-headers.yml
new file mode 100644
index 00000000000..74f6b5700f1
--- /dev/null
+++ b/changelogs/unreleased/bvl-handle-invalid-headers.yml
@@ -0,0 +1,5 @@
+---
+title: Handle nullbytes in auth headers
+merge_request: 46985
+author:
+type: fixed
diff --git a/changelogs/unreleased/nicolasdular-fix-bm-close-icon.yml b/changelogs/unreleased/nicolasdular-fix-bm-close-icon.yml
new file mode 100644
index 00000000000..e36c32fbeaf
--- /dev/null
+++ b/changelogs/unreleased/nicolasdular-fix-bm-close-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broadcast notification close icon appearance
+merge_request: 46804
+author:
+type: fixed
diff --git a/changelogs/unreleased/pl-fix-operations-settings-wo-pipelines.yml b/changelogs/unreleased/pl-fix-operations-settings-wo-pipelines.yml
new file mode 100644
index 00000000000..06af146b1a0
--- /dev/null
+++ b/changelogs/unreleased/pl-fix-operations-settings-wo-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Fix operations settings when Pipelines are disabled
+merge_request: 47062
+author:
+type: fixed
diff --git a/config/feature_flags/development/export_merge_requests_as_csv.yml b/config/feature_flags/development/export_merge_requests_as_csv.yml
deleted file mode 100644
index 73ed9137ffa..00000000000
--- a/config/feature_flags/development/export_merge_requests_as_csv.yml
+++ /dev/null
@@ -1,7 +0,0 @@
----
-name: export_merge_requests_as_csv
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45130
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267129
-type: development
-group: group::compliance
-default_enabled: true
diff --git a/doc/administration/audit_reports.md b/doc/administration/audit_reports.md
index 83fbeda26aa..9c772302375 100644
--- a/doc/administration/audit_reports.md
+++ b/doc/administration/audit_reports.md
@@ -18,12 +18,12 @@ needs.
## APIs
-- `https://docs.gitlab.com/ee/api/audit_events.html`
-- `https://docs.gitlab.com/ee/api/graphql/reference/#user`
-- `https://docs.gitlab.com/ee/api/graphql/reference/#groupmember`
-- `https://docs.gitlab.com/ee/api/graphql/reference/#projectmember`
+- [Audit events](../api/audit_events.md)
+- [GraphQL - User](../api/graphql/reference/index.md#user)
+- [GraphQL - GroupMember](../api/graphql/reference/index.md#groupmember)
+- [GraphQL - ProjectMember](../api/graphql/reference/index.md#projectmember)
## Features
-- `https://docs.gitlab.com/ee/administration/audit_events.html`
-- `https://docs.gitlab.com/ee/administration/logs.html`
+- [Audit events](audit_events.md)
+- [Log system](logs.md)
diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md
index 5bdabe547c4..4ebc541b756 100644
--- a/doc/administration/geo/disaster_recovery/index.md
+++ b/doc/administration/geo/disaster_recovery/index.md
@@ -133,6 +133,9 @@ Note the following when promoting a secondary:
```
1. Promote the **secondary** node to the **primary** node.
+ DANGER: **Warning:**
+ In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
+
CAUTION: **Caution:**
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
@@ -167,6 +170,9 @@ conjunction with multiple servers, as it can only
perform changes on a **secondary** with only a single machine. Instead, you must
do this manually.
+DANGER: **Warning:**
+In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
+
CAUTION: **Caution:**
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
diff --git a/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md b/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md
index c89b7929b13..daa13e52c0b 100644
--- a/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md
+++ b/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md
@@ -227,6 +227,9 @@ conjunction with multiple servers, as it can only
perform changes on a **secondary** with only a single machine. Instead, you must
do this manually.
+DANGER: **Warning:**
+In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
+
CAUTION: **Caution:**
If the secondary node [has been paused](../../../geo/index.md#pausing-and-resuming-replication), this performs
a point-in-time recovery to the last known state.
diff --git a/doc/administration/geo/index.md b/doc/administration/geo/index.md
index 085a2b52674..b9fffc9d4c2 100644
--- a/doc/administration/geo/index.md
+++ b/doc/administration/geo/index.md
@@ -196,6 +196,9 @@ For information on how to update your Geo nodes to the latest GitLab version, se
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35913) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
+DANGER: **Warning:**
+In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
+
CAUTION: **Caution:**
Pausing and resuming of replication is currently only supported for Geo installations using an
Omnibus GitLab-managed database. External databases are currently not supported.
diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md
index f207b5901e9..e01c60c0925 100644
--- a/doc/administration/geo/replication/version_specific_updates.md
+++ b/doc/administration/geo/replication/version_specific_updates.md
@@ -24,6 +24,13 @@ DROP SERVER gitlab_secondary CASCADE;
DROP EXTENSION IF EXISTS postgres_fdw;
```
+DANGER: **Warning:**
+In GitLab 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. To avoid this issue, upgrade to GitLab 13.4 or later.
+
+## Updating to GitLab 13.2
+
+In GitLab 13.2, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. To avoid this issue, upgrade to GitLab 13.4 or later.
+
## Updating to GitLab 13.0
Upgrading to GitLab 13.0 requires GitLab 12.10 to already be using PostgreSQL
diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md
index 4dac9f61469..554ee3b1028 100644
--- a/doc/api/releases/index.md
+++ b/doc/api/releases/index.md
@@ -367,7 +367,7 @@ POST /projects/:id/releases
| `assets:links` | array of hash | no | An array of assets links. |
| `assets:links:name`| string | required by: `assets:links` | The name of the link. |
| `assets:links:url` | string | required by: `assets:links` | The URL of the link. |
-| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md).
+| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets).
| `assets:links:link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`.
| `released_at` | datetime | no | The date when the release will be/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
diff --git a/doc/api/releases/links.md b/doc/api/releases/links.md
index 242b5eb41f5..2b33f6a4dc7 100644
--- a/doc/api/releases/links.md
+++ b/doc/api/releases/links.md
@@ -97,26 +97,29 @@ POST /projects/:id/releases/:tag_name/assets/links
| `tag_name` | string | yes | The tag associated with the Release. |
| `name` | string | yes | The name of the link. |
| `url` | string | yes | The URL of the link. |
+| `filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets).
| `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. |
Example request:
```shell
curl --request POST \
- --header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" \
- --data name="awesome-v0.2.dmg" \
- --data url="http://192.168.10.15:3000" \
- "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links"
+ --header "PRIVATE-TOKEN: tkhfG7HgG-LiZd3zfdDC" \
+ --data name="hellodarwin-amd64" \
+ --data url="https://gitlab.example.com/mynamespace/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64" \
+ --data filepath="/bin/hellodarwin-amd64" \
+ "https://gitlab.example.com/api/v4/projects/20/releases/v1.7.0/assets/links"
```
Example response:
```json
{
- "id":1,
- "name":"awesome-v0.2.dmg",
- "url":"http://192.168.10.15:3000",
- "external":true,
+ "id":2,
+ "name":"hellodarwin-amd64",
+ "url":"https://gitlab.example.com/mynamespace/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64",
+ "direct_asset_url":"https://gitlab.example.com/mynamespace/hello/-/releases/v1.7.0/downloads/bin/hellodarwin-amd64",
+ "external":false,
"link_type":"other"
}
```
@@ -136,6 +139,7 @@ PUT /projects/:id/releases/:tag_name/assets/links/:link_id
| `link_id` | integer | yes | The ID of the link. |
| `name` | string | no | The name of the link. |
| `url` | string | no | The URL of the link. |
+| `filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets).
| `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. |
NOTE: **Note:**
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index fbc8557fe39..10b7cfc06e1 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -320,7 +320,7 @@ services:
command: ["--registry-mirror", "https://registry-mirror.example.com"] # Specify the registry mirror to use.
```
-#### DinD service defined inside of GitLab Runner configuration
+##### DinD service defined inside of GitLab Runner configuration
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27173) in GitLab Runner 13.6.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 736a0414a3c..59f9dae660a 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -234,23 +234,23 @@ There are also two edge cases worth mentioning:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29654) in GitLab 12.5
-The top-level `workflow:` key applies to the entirety of a pipeline, and
-determines whether or not a pipeline is created. It accepts a single
-`rules:` key that operates similarly to [`rules:` defined within jobs](#rules),
-enabling dynamic configuration of the pipeline.
+The top-level `workflow:` keyword determines whether or not a pipeline is created.
+It accepts a single `rules:` keyword that is similar to [`rules:` defined within jobs](#rules).
+Use it to define what can trigger a new pipeline.
-If you are new to GitLab CI/CD and `workflow: rules`, you may find the [`workflow:rules` templates](#workflowrules-templates) useful.
+You can use the [`workflow:rules` templates](#workflowrules-templates) to import
+a preconfigured `workflow: rules` entry.
-To define your own `workflow: rules`, the available configuration options are:
+`workflow: rules` accepts these keywords:
-- [`if`](#rulesif): Define a rule.
-- [`when`](#when): May be set to `always` or `never` only. If not provided, the default value is `always`​.
+- [`if`](#rulesif): Check this rule to determine when to run a pipeline.
+- [`when`](#when): Specify what to do when the `if` rule evaluates to true.
+ - To run a pipeline, set to `always`.
+ - To prevent pipelines from running, set to `never`.
-If a pipeline attempts to run but matches no rule, it's dropped and doesn't run.
+When no rules evaluate to true, the pipeline does not run.
-Use the example rules below exactly as written to allow pipelines that match the rule
-to run. Add `when: never` to prevent pipelines that match the rule from running. See
-the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples.
+Some example `if` clauses for `workflow: rules`:
| Example rules | Details |
|------------------------------------------------------|-----------------------------------------------------------|
@@ -259,9 +259,12 @@ the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more exa
| `if: $CI_COMMIT_TAG` | Control when tag pipelines run. |
| `if: $CI_COMMIT_BRANCH` | Control when branch pipelines run. |
+See the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples.
+
For example, in the following configuration, pipelines run for all `push` events (changes to
-branches and new tags). Only push events with `-wip` in the commit message are excluded. Scheduled
-pipelines and merge request pipelines don't run, as there's no rule allowing them.
+branches and new tags). Pipelines for push events with `-wip` in the commit message
+don't run, because they are set to `when: never`. Pipelines for schedules or merge requests
+don't run either, because no rules evaluate to true for them:
```yaml
workflow:
@@ -271,11 +274,11 @@ workflow:
- if: '$CI_PIPELINE_SOURCE == "push"'
```
-This example has strict rules, and no other pipelines can run.
+This example has strict rules, and pipelines do **not** run in any other case.
-Alternatively, you can have loose rules by using only `when: never` rules, followed
-by a final `when: always` rule. This allows all types of pipelines, except for any
-that match the `when: never` rules:
+Alternatively, all of the rules can be `when: never`, with a final
+`when: always` rule. Pipelines that match the `when: never` rules do not run.
+All other pipeline types run:
```yaml
workflow:
@@ -287,12 +290,13 @@ workflow:
- when: always
```
-This example never allows pipelines for schedules or `push` (branches and tags) pipelines,
-but does allow pipelines in **all** other cases, *including* merge request pipelines.
+This example prevents pipelines for schedules or `push` (branches and tags) pipelines.
+The final `when: always` rule lets all other pipeline types run, **including** merge
+request pipelines.
-Be careful not to use a configuration that might run
-merge request pipelines and branch pipelines at the same time. As with `rules` defined in jobs,
-it can cause [duplicate pipelines](#prevent-duplicate-pipelines).
+Be careful not to have rules that match both branch pipelines
+and merge request pipelines. Similar to `rules` defined in jobs, this can cause
+[duplicate pipelines](#prevent-duplicate-pipelines).
#### `workflow:rules` templates
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index 8c0c189176c..1933b432138 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -7,14 +7,14 @@ type: howto, reference
# Edit files through the command line
-When [working with Git from the command line](start-using-git.md), you will need to
+When [working with Git from the command line](start-using-git.md), you need to
use more than just the Git commands. There are several basic commands that you should
learn, in order to make full use of the command line.
## Start working on your project
To work on a Git project locally (from your own computer), with the command line,
-first you will need to [clone (copy) it](start-using-git.md#clone-a-repository) to
+first you need to [clone (copy) it](start-using-git.md#clone-a-repository) to
your computer.
## Working with files on the command line
@@ -57,7 +57,7 @@ nano README.md
### Remove a file or directory
-It is easy to delete (remove) a file or directory, but be careful:
+It's easy to delete (remove) a file or directory, but be careful:
DANGER: **Warning:**
This will **permanently** delete a file.
@@ -96,7 +96,7 @@ for example) . Execute the same full command with:
Not all commands can be executed from a basic user account on a computer, you may
need administrator's rights to execute commands that affect the system, or try to access
protected data, for example. You can use `sudo` to execute these commands, but you
-will likely be asked for an administrator password.
+might be asked for an administrator password.
```shell
sudo RESTRICTED-COMMAND
@@ -108,8 +108,8 @@ damage to your data or system.
## Sample Git taskflow
-If you are completely new to Git, looking through some [sample taskflows](https://rogerdudler.github.io/git-guide/)
-will help you understand the best practices for using these commands as you work.
+If you're completely new to Git, looking through some [sample taskflows](https://rogerdudler.github.io/git-guide/)
+may help you understand the best practices for using these commands as you work.
<!-- ## Troubleshooting
diff --git a/doc/user/project/merge_requests/csv_export.md b/doc/user/project/merge_requests/csv_export.md
index d41a2567674..52c6f8a8d41 100644
--- a/doc/user/project/merge_requests/csv_export.md
+++ b/doc/user/project/merge_requests/csv_export.md
@@ -6,23 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Export Merge Requests to CSV **(CORE)**
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3619) in GitLab 13.6.
-> - It was [deployed behind a feature flag](../../../administration/feature_flags.md), disabled by default.
-> - Became enabled by default in GitLab 13.6.
-> - It's enabled on GitLab.com.
-> - It's recommended for production use.
-> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-export-merge-requests-to-csv). **(CORE ONLY)**
-> - It can be enabled or disabled for a single project.
-
-CAUTION: **Warning:**
-This feature might not be available to you. Check the **version history** note above for details.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3619) in GitLab 13.6.
Exporting Merge Requests CSV enables you and your team to export all the data collected from merge requests into a comma-separated values (CSV) file, which stores tabular data in plain text.
To export Merge Requests to CSV, navigate to your **Merge Requests** from the sidebar of a project and click **Export to CSV**.
-Exported files are generated asynchronously and delivered as an email attachment upon generation.
-
## CSV Output
The following table shows what attributes will be present in the CSV.
@@ -54,28 +43,3 @@ The following table shows what attributes will be present in the CSV.
- Export merge requests to CSV is not available at the Group’s merge request list.
- As the merge request CSV file is sent as an email attachment, the size is limited to 15MB to ensure successful delivery across a range of email providers. If you need to minimize the size of the file, you can narrow the search before export. For example, you can set up exports of open and closed merge requests in separate files.
-
-### Enable or disable Export Merge Requests to CSV **(CORE ONLY)**
-
-Export merge requests to CSV is under development but ready for production use.
-It is deployed behind a feature flag that is **enabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
-can opt to disable it.
-
-To enable it:
-
-```ruby
-# For the instance
-Feature.enable(:export_merge_requests_as_csv)
-# For a single project
-Feature.enable(:export_merge_requests_as_csv, Project.find(<project id>))
-```
-
-To disable it:
-
-```ruby
-# For the instance
-Feature.disable(:export_merge_requests_as_csv)
-# For a single project
-Feature.disable(:export_merge_requests_as_csv, Project.find(<project id>))
-```
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 64879b37dae..3e493f02392 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -32,6 +32,9 @@ To set up a project import/export:
Note the following:
+- Before you can import a project, you need to export the data first.
+ See [Exporting a project and its data](#exporting-a-project-and-its-data)
+ for how you can export a project through the UI.
- Imports from a newer version of GitLab are not supported.
The Importing GitLab version must be greater than or equal to the Exporting GitLab version.
- Imports will fail unless the import and export GitLab instances are
@@ -129,6 +132,11 @@ For more details on the specific data persisted in a project export, see the
## Exporting a project and its data
+Full project export functionality is limited to project maintainers and owners.
+You can configure such functionality through [project settings](index.md):
+
+To export a project and its data, follow these steps:
+
1. Go to your project's homepage.
1. Click **Settings** in the sidebar.
diff --git a/lib/gitlab/graphql/loaders/batch_model_loader.rb b/lib/gitlab/graphql/loaders/batch_model_loader.rb
index 164fe74148c..9b85ba164d4 100644
--- a/lib/gitlab/graphql/loaders/batch_model_loader.rb
+++ b/lib/gitlab/graphql/loaders/batch_model_loader.rb
@@ -12,14 +12,11 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def find
- BatchLoader::GraphQL.for({ model: model_class, id: model_id.to_i }).batch do |loader_info, loader|
- per_model = loader_info.group_by { |info| info[:model] }
- per_model.each do |model, info|
- ids = info.map { |i| i[:id] }
- results = model.where(id: ids)
+ BatchLoader::GraphQL.for(model_id.to_i).batch(key: model_class) do |ids, loader, args|
+ model = args[:key]
+ results = model.where(id: ids)
- results.each { |record| loader.call({ model: model, id: record.id }, record) }
- end
+ results.each { |record| loader.call(record.id, record) }
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/middleware/handle_malformed_strings.rb b/lib/gitlab/middleware/handle_malformed_strings.rb
index bb2a8ead525..9baa639caea 100644
--- a/lib/gitlab/middleware/handle_malformed_strings.rb
+++ b/lib/gitlab/middleware/handle_malformed_strings.rb
@@ -5,6 +5,8 @@ module Gitlab
# There is no valid reason for a request to contain a malformed string
# so just return HTTP 400 (Bad Request) if we receive one
class HandleMalformedStrings
+ include ActionController::HttpAuthentication::Basic
+
NULL_BYTE_REGEX = Regexp.new(Regexp.escape("\u0000")).freeze
attr_reader :app
@@ -21,16 +23,26 @@ module Gitlab
private
- def request_contains_malformed_string?(request)
+ def request_contains_malformed_string?(env)
return false if ENV['DISABLE_REQUEST_VALIDATION'] == '1'
- request = Rack::Request.new(request)
+ # Duplicate the env, so it is not modified when accessing the parameters
+ # https://github.com/rails/rails/blob/34991a6ae2fc68347c01ea7382fa89004159e019/actionpack/lib/action_dispatch/http/parameters.rb#L59
+ # The modification causes problems with our multipart middleware
+ request = ActionDispatch::Request.new(env.dup)
return true if malformed_path?(request.path)
+ return true if credentials_malformed?(request)
request.params.values.any? do |value|
param_has_null_byte?(value)
end
+ rescue ActionController::BadRequest
+ # If we can't build an ActionDispatch::Request something's wrong
+ # This would also happen if `#params` contains invalid UTF-8
+ # in this case we'll return a 400
+ #
+ true
end
def malformed_path?(path)
@@ -40,6 +52,13 @@ module Gitlab
true
end
+ def credentials_malformed?(request)
+ credentials = decode_credentials(request).presence
+ return false unless credentials
+
+ string_malformed?(credentials)
+ end
+
def param_has_null_byte?(value, depth = 0)
# Guard against possible attack sending large amounts of nested params
# Should be safe as deeply nested params are highly uncommon.
diff --git a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
index 7d019dc5130..da013a06777 100644
--- a/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/issue_activity_unique_counter.rb
@@ -9,14 +9,12 @@ module Gitlab
ISSUE_CREATED = 'g_project_management_issue_created'
ISSUE_CLOSED = 'g_project_management_issue_closed'
ISSUE_DESCRIPTION_CHANGED = 'g_project_management_issue_description_changed'
- ISSUE_ITERATION_CHANGED = 'g_project_management_issue_iteration_changed'
ISSUE_LABEL_CHANGED = 'g_project_management_issue_label_changed'
ISSUE_MADE_CONFIDENTIAL = 'g_project_management_issue_made_confidential'
ISSUE_MADE_VISIBLE = 'g_project_management_issue_made_visible'
ISSUE_MILESTONE_CHANGED = 'g_project_management_issue_milestone_changed'
ISSUE_REOPENED = 'g_project_management_issue_reopened'
ISSUE_TITLE_CHANGED = 'g_project_management_issue_title_changed'
- ISSUE_WEIGHT_CHANGED = 'g_project_management_issue_weight_changed'
ISSUE_CROSS_REFERENCED = 'g_project_management_issue_cross_referenced'
ISSUE_MOVED = 'g_project_management_issue_moved'
ISSUE_RELATED = 'g_project_management_issue_related'
@@ -24,9 +22,6 @@ module Gitlab
ISSUE_MARKED_AS_DUPLICATE = 'g_project_management_issue_marked_as_duplicate'
ISSUE_LOCKED = 'g_project_management_issue_locked'
ISSUE_UNLOCKED = 'g_project_management_issue_unlocked'
- ISSUE_ADDED_TO_EPIC = 'g_project_management_issue_added_to_epic'
- ISSUE_REMOVED_FROM_EPIC = 'g_project_management_issue_removed_from_epic'
- ISSUE_CHANGED_EPIC = 'g_project_management_issue_changed_epic'
ISSUE_DESIGNS_ADDED = 'g_project_management_issue_designs_added'
ISSUE_DESIGNS_MODIFIED = 'g_project_management_issue_designs_modified'
ISSUE_DESIGNS_REMOVED = 'g_project_management_issue_designs_removed'
@@ -78,14 +73,6 @@ module Gitlab
track_unique_action(ISSUE_MILESTONE_CHANGED, author, time)
end
- def track_issue_iteration_changed_action(author:, time: Time.zone.now)
- track_unique_action(ISSUE_ITERATION_CHANGED, author, time)
- end
-
- def track_issue_weight_changed_action(author:, time: Time.zone.now)
- track_unique_action(ISSUE_WEIGHT_CHANGED, author, time)
- end
-
def track_issue_cross_referenced_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_CROSS_REFERENCED, author, time)
end
@@ -114,18 +101,6 @@ module Gitlab
track_unique_action(ISSUE_UNLOCKED, author, time)
end
- def track_issue_added_to_epic_action(author:, time: Time.zone.now)
- track_unique_action(ISSUE_ADDED_TO_EPIC, author, time)
- end
-
- def track_issue_removed_from_epic_action(author:, time: Time.zone.now)
- track_unique_action(ISSUE_REMOVED_FROM_EPIC, author, time)
- end
-
- def track_issue_changed_epic_action(author:, time: Time.zone.now)
- track_unique_action(ISSUE_CHANGED_EPIC, author, time)
- end
-
def track_issue_designs_added_action(author:, time: Time.zone.now)
track_unique_action(ISSUE_DESIGNS_ADDED, author, time)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b4898eff180..af33e6cb876 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2551,6 +2551,9 @@ msgstr ""
msgid "AlertSettings|Copy"
msgstr ""
+msgid "AlertSettings|Delete integration"
+msgstr ""
+
msgid "AlertSettings|Enter integration name"
msgstr ""
@@ -2668,9 +2671,21 @@ msgstr ""
msgid "AlertsIntegrations|Prometheus"
msgstr ""
+msgid "AlertsIntegrations|The integration could not be added. Please try again."
+msgstr ""
+
+msgid "AlertsIntegrations|The integration could not be deleted. Please try again."
+msgstr ""
+
+msgid "AlertsIntegrations|The integration has been successfully removed."
+msgstr ""
+
msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list."
msgstr ""
+msgid "AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone."
+msgstr ""
+
msgid "Algorithm"
msgstr ""
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 31ce37a0607..d7d1278159b 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1998,10 +1998,6 @@ RSpec.describe Projects::MergeRequestsController do
describe 'POST export_csv' do
subject { post :export_csv, params: { namespace_id: project.namespace, project_id: project } }
- before do
- stub_feature_flags(export_merge_requests_as_csv: project)
- end
-
it 'redirects to the merge request index' do
subject
@@ -2014,17 +2010,5 @@ RSpec.describe Projects::MergeRequestsController do
subject
end
-
- context 'feature is disabled' do
- before do
- stub_feature_flags(export_merge_requests_as_csv: false)
- end
-
- it 'expects a 404 response' do
- subject
-
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
end
end
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index a47f2285e37..952a78ec79a 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -52,29 +52,20 @@ RSpec.describe 'Dashboard Merge Requests' do
end
context 'merge requests exist' do
- let_it_be(:author_user) { create(:user) }
let(:label) { create(:label) }
let!(:assigned_merge_request) do
create(:merge_request,
assignees: [current_user],
source_project: project,
- author: author_user)
- end
-
- let!(:review_requested_merge_request) do
- create(:merge_request,
- reviewers: [current_user],
- source_branch: 'review',
- source_project: project,
- author: author_user)
+ author: create(:user))
end
let!(:assigned_merge_request_from_fork) do
create(:merge_request,
source_branch: 'markdown', assignees: [current_user],
target_project: public_project, source_project: forked_project,
- author: author_user)
+ author: create(:user))
end
let!(:authored_merge_request) do
@@ -103,7 +94,7 @@ RSpec.describe 'Dashboard Merge Requests' do
create(:merge_request,
source_branch: 'fix',
source_project: project,
- author: author_user)
+ author: create(:user))
end
before do
@@ -120,10 +111,6 @@ RSpec.describe 'Dashboard Merge Requests' do
expect(page).not_to have_content(labeled_merge_request.title)
end
- it 'shows review requested merge requests' do
- expect(page).to have_content(review_requested_merge_request.title)
- end
-
it 'shows authored merge requests', :js do
reset_filters
input_filtered_search("author:=#{current_user.to_reference}")
diff --git a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
index e9e24c12af1..b3ace2e30ff 100644
--- a/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
+++ b/spec/features/file_uploads/multipart_invalid_uploads_spec.rb
@@ -22,13 +22,13 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
)
end
- RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil|
+ RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil, status: 500|
context "with invalid key #{key_name}" do
let(:body) { { key_name => file, 'package[test][name]' => 'test' } }
it { expect { subject }.not_to change { Packages::Package.nuget.count } }
- it { expect(subject.code).to eq(500) }
+ it { expect(subject.code).to eq(status) }
it { expect(subject.body).to include(message.presence || "invalid field: \"#{key_name}\"") }
end
@@ -45,7 +45,7 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
# These keys are rejected directly by rack itself.
# The request will not be received by multipart.rb (can't use the 'handling file uploads' shared example)
it_behaves_like 'rejecting invalid keys', key_name: 'x' * 11000, message: 'Puma caught this error: exceeded available parameter key space (RangeError)'
- it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', message: 'Puma caught this error: expected Hash (got Array)'
+ it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', status: 400, message: 'Bad Request'
it_behaves_like 'handling file uploads', 'by rejecting uploads with an invalid key'
end
diff --git a/spec/features/merge_requests/user_exports_as_csv_spec.rb b/spec/features/merge_requests/user_exports_as_csv_spec.rb
index 63ed1cb5231..a86ff9d7335 100644
--- a/spec/features/merge_requests/user_exports_as_csv_spec.rb
+++ b/spec/features/merge_requests/user_exports_as_csv_spec.rb
@@ -9,38 +9,23 @@ RSpec.describe 'Merge Requests > Exports as CSV', :js do
before do
sign_in(user)
+ visit(project_merge_requests_path(project))
end
subject { page.find('.nav-controls') }
- context 'feature is not enabled' do
- before do
- stub_feature_flags(export_merge_requests_as_csv: false)
- visit(project_merge_requests_path(project))
- end
-
- it { is_expected.not_to have_button('Export as CSV') }
- end
+ it { is_expected.to have_button('Export as CSV') }
- context 'feature is enabled for a project' do
+ context 'button is clicked' do
before do
- stub_feature_flags(export_merge_requests_as_csv: project)
- visit(project_merge_requests_path(project))
+ click_button('Export as CSV')
end
- it { is_expected.to have_button('Export as CSV') }
-
- context 'button is clicked' do
- before do
- click_button('Export as CSV')
- end
-
- it 'shows a success message' do
- click_link('Export merge requests')
+ it 'shows a success message' do
+ click_link('Export merge requests')
- expect(page).to have_content 'Your CSV export has started.'
- expect(page).to have_content "It will be emailed to #{user.email} when complete"
- end
+ expect(page).to have_content 'Your CSV export has started.'
+ expect(page).to have_content "It will be emailed to #{user.email} when complete"
end
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 3ab1b26d71d..68958e37001 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -333,8 +333,6 @@ RSpec.describe MergeRequestsFinder do
end
context 'assignee filtering' do
- let_it_be(:user3) { create(:user) }
-
let(:issuables) { described_class.new(user, params).execute }
it_behaves_like 'assignee ID filter' do
@@ -353,6 +351,7 @@ RSpec.describe MergeRequestsFinder do
merge_request3.assignees = [user2, user3]
end
+ let_it_be(:user3) { create(:user) }
let(:params) { { assignee_username: [user2.username, user3.username] } }
let(:expected_issuables) { [merge_request3] }
end
@@ -367,6 +366,7 @@ RSpec.describe MergeRequestsFinder do
end
it_behaves_like 'no assignee filter' do
+ let_it_be(:user3) { create(:user) }
let(:expected_issuables) { [merge_request4, merge_request5] }
end
@@ -374,54 +374,30 @@ RSpec.describe MergeRequestsFinder do
let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
end
- context 'with just reviewers' do
- it_behaves_like 'assignee username filter' do
- before do
- merge_request4.reviewers = [user3]
- merge_request4.assignees = []
- end
+ context 'filtering by group milestone' do
+ let(:group_milestone) { create(:milestone, group: group) }
- let(:params) { { assignee_username: [user3.username] } }
- let(:expected_issuables) { [merge_request4] }
+ before do
+ merge_request1.update!(milestone: group_milestone)
+ merge_request2.update!(milestone: group_milestone)
end
- end
- context 'with an additional reviewer' do
- it_behaves_like 'assignee username filter' do
- before do
- merge_request3.assignees = [user3]
- merge_request4.reviewers = [user3]
- end
+ it 'returns merge requests assigned to that group milestone' do
+ params = { milestone_title: group_milestone.title }
- let(:params) { { assignee_username: [user3.username] } }
- let(:expected_issuables) { [merge_request3, merge_request4] }
- end
- end
- end
-
- context 'filtering by group milestone' do
- let(:group_milestone) { create(:milestone, group: group) }
-
- before do
- merge_request1.update!(milestone: group_milestone)
- merge_request2.update!(milestone: group_milestone)
- end
-
- it 'returns merge requests assigned to that group milestone' do
- params = { milestone_title: group_milestone.title }
-
- merge_requests = described_class.new(user, params).execute
+ merge_requests = described_class.new(user, params).execute
- expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
- end
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
+ end
- context 'using NOT' do
- let(:params) { { not: { milestone_title: group_milestone.title } } }
+ context 'using NOT' do
+ let(:params) { { not: { milestone_title: group_milestone.title } } }
- it 'returns MRs not assigned to that group milestone' do
- merge_requests = described_class.new(user, params).execute
+ it 'returns MRs not assigned to that group milestone' do
+ merge_requests = described_class.new(user, params).execute
- expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
+ expect(merge_requests).to contain_exactly(merge_request3, merge_request4, merge_request5)
+ end
end
end
end
@@ -587,27 +563,6 @@ RSpec.describe MergeRequestsFinder do
expect(mrs).to eq([mr2])
end
end
-
- it 'does not raise any exception with complex filters' do
- # available filters from MergeRequest dashboard UI
- params = {
- project_id: project1.id,
- scope: 'authored',
- state: 'opened',
- author_username: user.username,
- assignee_username: user.username,
- approver_usernames: [user.username],
- approved_by_usernames: [user.username],
- milestone_title: 'none',
- release_tag: 'none',
- label_names: 'none',
- my_reaction_emoji: 'none',
- draft: 'no'
- }
-
- merge_requests = described_class.new(user, params).execute
- expect { merge_requests.load }.not_to raise_error
- end
end
describe '#row_count', :request_store do
diff --git a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js
index 8d14babf4af..59238e40686 100644
--- a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js
+++ b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js
@@ -128,18 +128,18 @@ describe('AlertsSettingsFormNew', () => {
it('allows for update-integration with the correct form values for HTTP', async () => {
createComponent({
+ data: {
+ selectedIntegration: typeSet.http,
+ },
props: {
- currentIntegration: { id: '1' },
+ currentIntegration: { id: '1', name: 'Test integration pre' },
loading: false,
},
});
- const options = findSelect().findAll('option');
- await options.at(1).setSelected();
-
await findFormFields()
.at(0)
- .setValue('Test integration');
+ .setValue('Test integration post');
await findFormToggle().trigger('click');
await wrapper.vm.$nextTick();
@@ -153,27 +153,27 @@ describe('AlertsSettingsFormNew', () => {
expect(wrapper.emitted('update-integration')).toBeTruthy();
expect(wrapper.emitted('update-integration')[0]).toEqual([
- { type: typeSet.http, variables: { name: 'Test integration', active: true } },
+ { type: typeSet.http, variables: { name: 'Test integration post', active: true } },
]);
});
it('allows for update-integration with the correct form values for PROMETHEUS', async () => {
createComponent({
+ data: {
+ selectedIntegration: typeSet.prometheus,
+ },
props: {
- currentIntegration: { id: '1' },
+ currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
loading: false,
},
});
- const options = findSelect().findAll('option');
- await options.at(2).setSelected();
-
await findFormFields()
.at(0)
.setValue('Test integration');
await findFormFields()
.at(1)
- .setValue('https://test.com');
+ .setValue('https://test-post.com');
await findFormToggle().trigger('click');
await wrapper.vm.$nextTick();
@@ -187,7 +187,7 @@ describe('AlertsSettingsFormNew', () => {
expect(wrapper.emitted('update-integration')).toBeTruthy();
expect(wrapper.emitted('update-integration')[0]).toEqual([
- { type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } },
+ { type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', active: true } },
]);
});
});
diff --git a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js
index 5fa21d01340..e2e7034940d 100644
--- a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js
@@ -1,13 +1,17 @@
-import { mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { mount, createLocalVue } from '@vue/test-utils';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
import { GlLoadingIcon } from '@gitlab/ui';
import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue';
import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
+import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql';
import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql';
+import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql';
import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql';
import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql';
import { typeSet } from '~/alerts_settings/constants';
@@ -20,16 +24,34 @@ import {
createPrometheusVariables,
updatePrometheusVariables,
ID,
+ errorMsg,
+ getIntegrationsQueryResponse,
+ destroyIntegrationResponse,
+ integrationToDestroy,
+ destroyIntegrationResponseWithErrors,
} from './mocks/apollo_mock';
jest.mock('~/flash');
+const localVue = createLocalVue();
+
describe('AlertsSettingsWrapper', () => {
let wrapper;
+ let fakeApollo;
+ let destroyIntegrationHandler;
const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon);
const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr');
+ async function destroyHttpIntegration(localWrapper) {
+ await jest.runOnlyPendingTimers();
+ await localWrapper.vm.$nextTick();
+
+ localWrapper
+ .find(IntegrationsList)
+ .vm.$emit('delete-integration', { id: integrationToDestroy.id });
+ }
+
const createComponent = ({ data = {}, provide = {}, loading = false } = {}) => {
wrapper = mount(AlertsSettingsWrapper, {
data() {
@@ -54,6 +76,29 @@ describe('AlertsSettingsWrapper', () => {
});
};
+ function createComponentWithApollo({
+ destroyHandler = jest.fn().mockResolvedValue(destroyIntegrationResponse),
+ } = {}) {
+ localVue.use(VueApollo);
+ destroyIntegrationHandler = destroyHandler;
+
+ const requestHandlers = [
+ [getIntegrationsQuery, jest.fn().mockResolvedValue(getIntegrationsQueryResponse)],
+ [destroyHttpIntegrationMutation, destroyIntegrationHandler],
+ ];
+
+ fakeApollo = createMockApollo(requestHandlers);
+
+ wrapper = mount(AlertsSettingsWrapper, {
+ localVue,
+ apolloProvider: fakeApollo,
+ provide: {
+ ...defaultAlertSettingsConfig,
+ glFeatures: { httpIntegrationsList: true },
+ },
+ });
+ }
+
afterEach(() => {
if (wrapper) {
wrapper.destroy();
@@ -243,7 +288,6 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows error alert when integration creation fails ', async () => {
- const errorMsg = 'Something went wrong';
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
provide: { glFeatures: { httpIntegrationsList: true } },
@@ -259,7 +303,6 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows error alert when integration token reset fails ', () => {
- const errorMsg = 'Something went wrong';
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
provide: { glFeatures: { httpIntegrationsList: true } },
@@ -276,7 +319,6 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows error alert when integration update fails ', () => {
- const errorMsg = 'Something went wrong';
createComponent({
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
provide: { glFeatures: { httpIntegrationsList: true } },
@@ -292,4 +334,41 @@ describe('AlertsSettingsWrapper', () => {
});
});
});
+
+ describe('with mocked Apollo client', () => {
+ it('has a selection of integrations loaded via the getIntegrationsQuery', async () => {
+ createComponentWithApollo();
+
+ await jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ expect(findIntegrations()).toHaveLength(4);
+ });
+
+ it('calls a mutation with correct parameters and destroys a integration', async () => {
+ createComponentWithApollo();
+
+ await destroyHttpIntegration(wrapper);
+
+ expect(destroyIntegrationHandler).toHaveBeenCalled();
+
+ await wrapper.vm.$nextTick();
+
+ expect(findIntegrations()).toHaveLength(3);
+ });
+
+ it('displays flash if mutation had a recoverable error', async () => {
+ createComponentWithApollo({
+ destroyHandler: jest.fn().mockResolvedValue(destroyIntegrationResponseWithErrors),
+ });
+
+ await destroyHttpIntegration(wrapper);
+
+ await wrapper.vm.$nextTick(); // kick off the DOM update
+ await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
+ await wrapper.vm.$nextTick(); // kick off the DOM update for flash
+
+ expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
+ });
+ });
});
diff --git a/spec/frontend/alerts_settings/mocks/apollo_mock.js b/spec/frontend/alerts_settings/mocks/apollo_mock.js
index d4cf0627c66..e0eba1e8421 100644
--- a/spec/frontend/alerts_settings/mocks/apollo_mock.js
+++ b/spec/frontend/alerts_settings/mocks/apollo_mock.js
@@ -1,5 +1,6 @@
const projectPath = '';
export const ID = 'gid://gitlab/AlertManagement::HttpIntegration/7';
+export const errorMsg = 'Something went wrong';
export const createHttpVariables = {
name: 'Test Pre',
@@ -24,3 +25,99 @@ export const updatePrometheusVariables = {
active: true,
id: ID,
};
+
+export const getIntegrationsQueryResponse = {
+ data: {
+ project: {
+ alertManagementIntegrations: {
+ nodes: [
+ {
+ id: '37',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 5',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
+ token: '89eb01df471d990ff5162a1c640408cf',
+ apiUrl: null,
+ },
+ {
+ id: '41',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 9999',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-9999/b78a566e1776cfc2.json',
+ token: 'f7579aa03844e07af3b1f0fca3f79f81',
+ apiUrl: null,
+ },
+ {
+ id: '40',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 6',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-6/3e828ae28a240222.json',
+ token: '6536102a607a5dd74fcdde921f2349ee',
+ apiUrl: null,
+ },
+ {
+ id: '12',
+ type: 'PROMETHEUS',
+ active: false,
+ name: 'Prometheus',
+ url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/prometheus/alerts/notify.json',
+ token: '256f687c6225aa5d6ee50c3d68120c4c',
+ apiUrl: 'https://localhost.ieeeesassadasasa',
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const integrationToDestroy = {
+ id: '37',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 5',
+ url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
+ token: '89eb01df471d990ff5162a1c640408cf',
+ apiUrl: null,
+};
+
+export const destroyIntegrationResponse = {
+ data: {
+ httpIntegrationDestroy: {
+ errors: [],
+ integration: {
+ id: '37',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 5',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
+ token: '89eb01df471d990ff5162a1c640408cf',
+ apiUrl: null,
+ },
+ },
+ },
+};
+
+export const destroyIntegrationResponseWithErrors = {
+ data: {
+ httpIntegrationDestroy: {
+ errors: ['Houston, we have a problem'],
+ integration: {
+ id: '37',
+ type: 'HTTP',
+ active: true,
+ name: 'Test 5',
+ url:
+ 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
+ token: '89eb01df471d990ff5162a1c640408cf',
+ apiUrl: null,
+ },
+ },
+ },
+};
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
index ef22979ca7d..3276ef911e3 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
@@ -22,7 +22,7 @@ describe('tags list row', () => {
let wrapper;
const [tag] = [...tagsListResponse.data];
- const defaultProps = { tag, isDesktop: true, index: 0 };
+ const defaultProps = { tag, isMobile: false, index: 0 };
const findCheckbox = () => wrapper.find(GlFormCheckbox);
const findName = () => wrapper.find('[data-testid="name"]');
@@ -114,7 +114,7 @@ describe('tags list row', () => {
});
it('on mobile has mw-s class', () => {
- mountComponent({ ...defaultProps, isDesktop: false });
+ mountComponent({ ...defaultProps, isMobile: true });
expect(findName().classes('mw-s')).toBe(true);
});
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
index 401202026bb..ebeaa8ff870 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js
@@ -14,7 +14,7 @@ describe('Tags List', () => {
const findDeleteButton = () => wrapper.find(GlButton);
const findListTitle = () => wrapper.find('[data-testid="list-title"]');
- const mountComponent = (propsData = { tags, isDesktop: true }) => {
+ const mountComponent = (propsData = { tags, isMobile: false }) => {
wrapper = shallowMount(component, {
propsData,
});
@@ -41,15 +41,15 @@ describe('Tags List', () => {
describe('delete button', () => {
it.each`
- inputTags | isDesktop | isVisible
- ${tags} | ${true} | ${true}
- ${tags} | ${false} | ${false}
- ${readOnlyTags} | ${true} | ${false}
- ${readOnlyTags} | ${false} | ${false}
+ inputTags | isMobile | isVisible
+ ${tags} | ${false} | ${true}
+ ${tags} | ${true} | ${false}
+ ${readOnlyTags} | ${false} | ${false}
+ ${readOnlyTags} | ${true} | ${false}
`(
- 'is $isVisible that delete button exists when tags is $inputTags and isDesktop is $isDesktop',
- ({ inputTags, isDesktop, isVisible }) => {
- mountComponent({ tags: inputTags, isDesktop });
+ 'is $isVisible that delete button exists when tags is $inputTags and isMobile is $isMobile',
+ ({ inputTags, isMobile, isVisible }) => {
+ mountComponent({ tags: inputTags, isMobile });
expect(findDeleteButton().exists()).toBe(isVisible);
},
@@ -110,12 +110,6 @@ describe('Tags List', () => {
expect(rows.at(0).attributes()).toMatchObject({
first: 'true',
- isdesktop: 'true',
- });
-
- // The list has only two tags and for some reasons .at(-1) does not work
- expect(rows.at(1).attributes()).toMatchObject({
- isdesktop: 'true',
});
});
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index 86b52c4f06a..372875914a5 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -124,7 +124,7 @@ describe('Details Page', () => {
it('has the correct props bound', () => {
expect(findTagsList().props()).toMatchObject({
- isDesktop: true,
+ isMobile: false,
tags: store.state.tags,
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
index 5c7e6a87c16..56832f82b05 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedDropdownItem } from '@gitlab/ui';
+import { GlDropdownItem } from '@gitlab/ui';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
const commits = [
@@ -39,7 +39,7 @@ describe('Commits message dropdown component', () => {
wrapper.destroy();
});
- const findDropdownElements = () => wrapper.findAll(GlDeprecatedDropdownItem);
+ const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
it('should have 3 elements in dropdown list', () => {
diff --git a/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb
index cf1f00bc176..7ae33346388 100644
--- a/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb
+++ b/spec/lib/gitlab/graphql/loaders/batch_model_loader_spec.rb
@@ -4,8 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::Graphql::Loaders::BatchModelLoader do
describe '#find' do
- let(:issue) { create(:issue) }
- let(:user) { create(:user) }
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:other_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
it 'finds a model by id' do
issue_result = described_class.new(Issue, issue.id).find
@@ -16,15 +17,25 @@ RSpec.describe Gitlab::Graphql::Loaders::BatchModelLoader do
end
it 'only queries once per model' do
- other_user = create(:user)
- user
- issue
-
expect do
[described_class.new(User, other_user.id).find,
described_class.new(User, user.id).find,
described_class.new(Issue, issue.id).find].map(&:sync)
end.not_to exceed_query_limit(2)
end
+
+ it 'does not force values unnecessarily' do
+ expect do
+ a = described_class.new(User, user.id).find
+ b = described_class.new(Issue, issue.id).find
+
+ b.sync
+
+ c = described_class.new(User, other_user.id).find
+
+ a.sync
+ c.sync
+ end.not_to exceed_query_limit(2)
+ end
end
end
diff --git a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb
index 5ed1580fa8d..fec273ecafd 100644
--- a/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb
+++ b/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb
@@ -4,6 +4,8 @@ require 'spec_helper'
require "rack/test"
RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
+ include GitHttpHelpers
+
let(:null_byte) { "\u0000" }
let(:escaped_null_byte) { "%00" }
let(:invalid_string) { "mal\xC0formed" }
@@ -57,6 +59,22 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
end
end
+ context 'in authorization headers' do
+ let(:problematic_input) { null_byte }
+
+ it 'rejects problematic input in the password' do
+ env = env_for.merge(auth_env("username", "password#{problematic_input}encoded", nil))
+
+ expect(subject.call(env)).to eq error_400
+ end
+
+ it 'rejects problematic input in the password' do
+ env = env_for.merge(auth_env("username#{problematic_input}", "password#{problematic_input}encoded", nil))
+
+ expect(subject.call(env)).to eq error_400
+ end
+ end
+
context 'in params' do
shared_examples_for 'checks params' do
it 'rejects bad params in a top level param' do
@@ -86,24 +104,24 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
expect(subject.call(env)).to eq error_400
end
+ end
+
+ context 'with null byte' do
+ let(:problematic_input) { null_byte }
+
+ it_behaves_like 'checks params'
it "gives up and does not reject too deeply nested params" do
env = env_for(name: [
- {
- inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{problematic_input} bad" }] }
- }
- ])
+ {
+ inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{problematic_input} bad" }] }
+ }
+ ])
expect(subject.call(env)).not_to eq error_400
end
end
- context 'with null byte' do
- it_behaves_like 'checks params' do
- let(:problematic_input) { null_byte }
- end
- end
-
context 'with malformed strings' do
it_behaves_like 'checks params' do
let(:problematic_input) { invalid_string }
@@ -124,4 +142,10 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
expect(subject.call(env)).not_to eq error_400
end
end
+
+ it 'does not modify the env' do
+ env = env_for
+
+ expect { subject.call(env) }.not_to change { env }
+ end
end
diff --git a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
index c86b3a01d1e..803eff05efe 100644
--- a/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/issue_activity_unique_counter_spec.rb
@@ -168,36 +168,6 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
end
end
- context 'for Issue added to epic actions' do
- it_behaves_like 'a tracked issue edit event' do
- let(:action) { described_class::ISSUE_ADDED_TO_EPIC}
-
- def track_action(params)
- described_class.track_issue_added_to_epic_action(**params)
- end
- end
- end
-
- context 'for Issue removed from epic actions' do
- it_behaves_like 'a tracked issue edit event' do
- let(:action) { described_class::ISSUE_REMOVED_FROM_EPIC}
-
- def track_action(params)
- described_class.track_issue_removed_from_epic_action(**params)
- end
- end
- end
-
- context 'for Issue changed epic actions' do
- it_behaves_like 'a tracked issue edit event' do
- let(:action) { described_class::ISSUE_CHANGED_EPIC}
-
- def track_action(params)
- described_class.track_issue_changed_epic_action(**params)
- end
- end
- end
-
context 'for Issue designs added actions' do
it_behaves_like 'a tracked issue edit event' do
let(:action) { described_class::ISSUE_DESIGNS_ADDED }
diff --git a/spec/requests/user_sends_malformed_strings_spec.rb b/spec/requests/user_sends_malformed_strings_spec.rb
index b6eda9159bc..da533606be5 100644
--- a/spec/requests/user_sends_malformed_strings_spec.rb
+++ b/spec/requests/user_sends_malformed_strings_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe 'User sends malformed strings as params' do
+RSpec.describe 'User sends malformed strings' do
+ include GitHttpHelpers
+
let(:null_byte) { "\u0000" }
let(:invalid_string) { "mal\xC0formed" }
@@ -17,4 +19,10 @@ RSpec.describe 'User sends malformed strings as params' do
expect(response).to have_gitlab_http_status(:bad_request)
end
+
+ it 'raises a 400 error with null bytes in the auth headers' do
+ clone_get("project/path", user: "hello#{null_byte}", password: "nothing to see")
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb
index 24ab64b20f5..facb4e2016d 100644
--- a/spec/views/projects/settings/operations/show.html.haml_spec.rb
+++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'projects/settings/operations/show' do
end
before_all do
- project.add_reporter(user)
+ project.add_maintainer(user)
end
before do