summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 12:07:12 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 12:07:12 +0000
commit43771438e9ccf20d1b6cf12b690e63844d7c3d49 (patch)
tree147aefba22d99be62ff3c112f50e205e486e58c7
parenteeb25534bae1021f5b7940138ee56dea8fc79949 (diff)
downloadgitlab-ce-43771438e9ccf20d1b6cf12b690e63844d7c3d49.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml5
-rw-r--r--.rubocop_todo/layout/empty_line_after_magic_comment.yml5
-rw-r--r--.rubocop_todo/rspec/factory_bot/local_static_assignment.yml3
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue49
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_checkbox.vue4
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue36
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js6
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue121
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/refs_list.vue91
-rw-r--r--app/assets/javascripts/projects/commit_box/info/constants.js14
-rw-r--r--app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_branches.query.graphql10
-rw-r--r--app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_tags.query.graphql10
-rw-r--r--app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_references.query.graphql19
-rw-r--r--app/assets/javascripts/projects/commit_box/info/index.js6
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_references.js32
-rw-r--r--app/assets/javascripts/projects/commit_box/info/load_branches.js24
-rw-r--r--app/services/alert_management/process_prometheus_alert_service.rb6
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb6
-rw-r--r--app/views/projects/commit/_commit_box.html.haml5
-rw-r--r--config/feature_flags/development/password_reset_any_verified_email.yml2
-rw-r--r--db/migrate/20230512141931_add_group_id_to_dependency_list_exports.rb7
-rw-r--r--db/post_migrate/20230512143000_remove_dependency_list_exports_project_id_not_null_constraint.rb11
-rw-r--r--db/post_migrate/20230515101208_index_group_id_on_dependency_list_exports.rb15
-rw-r--r--db/post_migrate/20230515102353_add_foreign_key_to_group_id_on_dependency_list_exports.rb17
-rw-r--r--db/post_migrate/20230517001535_prepare_async_index_for_ci_pipeline_variables_bigint_id.rb15
-rw-r--r--db/schema_migrations/202305121419311
-rw-r--r--db/schema_migrations/202305121430001
-rw-r--r--db/schema_migrations/202305151012081
-rw-r--r--db/schema_migrations/202305151023531
-rw-r--r--db/schema_migrations/202305170015351
-rw-r--r--db/structure.sql8
-rw-r--r--doc/administration/gitaly/troubleshooting.md4
-rw-r--r--doc/integration/azure.md28
-rw-r--r--doc/user/product_analytics/index.md38
-rw-r--r--lib/gitlab/alert_management/payload/base.rb1
-rw-r--r--lib/gitlab/alert_management/payload/prometheus.rb6
-rw-r--r--lib/sidebars/user_profile/panel.rb3
-rw-r--r--locale/gitlab.pot18
-rw-r--r--rubocop/cop/rspec/factory_bot/local_static_assignment.rb67
-rw-r--r--spec/controllers/projects/merge_requests/conflicts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/content_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/drafts_controller_spec.rb2
-rw-r--r--spec/factories/design_management/designs.rb2
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js254
-rw-r--r--spec/frontend/ci/artifacts/components/job_checkbox_spec.js6
-rw-r--r--spec/frontend/commit/components/commit_refs_spec.js82
-rw-r--r--spec/frontend/commit/components/refs_list_spec.js70
-rw-r--r--spec/frontend/commit/mock_data.js57
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js35
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js12
-rw-r--r--spec/frontend/projects/commit_box/info/load_branches_spec.js86
-rw-r--r--spec/lib/gitlab/alert_management/payload/prometheus_spec.rb14
-rw-r--r--spec/requests/api/internal/base_spec.rb73
-rw-r--r--spec/rubocop/cop/rspec/factory_bot/local_static_assignment_spec.rb62
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb9
56 files changed, 1237 insertions, 230 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 9ceb8466270..c6d2d48db9b 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1012,3 +1012,8 @@ Graphql/ResourceNotAvailableError:
Exclude:
# Definition of `raise_resource_not_available_error!`
- 'lib/gitlab/graphql/authorize/authorize_resource.rb'
+
+RSpec/FactoryBot/LocalStaticAssignment:
+ Include:
+ - spec/factories/**/*.rb
+ - ee/spec/factories/**/*.rb
diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
index 536f13e3919..f8f4f1d3b8a 100644
--- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml
+++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
@@ -271,11 +271,6 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/lib/gitlab/cidr.rb'
- 'ee/lib/quality/seeders/vulnerabilities.rb'
- 'ee/spec/components/billing/plan_component_spec.rb'
- - 'ee/spec/components/namespaces/storage/limit_alert_component_spec.rb'
- - 'ee/spec/components/namespaces/storage/pre_enforcement_alert_component_spec.rb'
- - 'ee/spec/components/namespaces/storage/project_pre_enforcement_alert_component_spec.rb'
- - 'ee/spec/components/namespaces/storage/subgroup_pre_enforcement_alert_component_spec.rb'
- - 'ee/spec/components/namespaces/storage/user_pre_enforcement_alert_component_spec.rb'
- 'ee/spec/controllers/admin/geo/nodes_controller_spec.rb'
- 'ee/spec/controllers/ee/projects/autocomplete_sources_controller_spec.rb'
- 'ee/spec/controllers/ee/projects/protected_branches_controller_spec.rb'
diff --git a/.rubocop_todo/rspec/factory_bot/local_static_assignment.yml b/.rubocop_todo/rspec/factory_bot/local_static_assignment.yml
new file mode 100644
index 00000000000..7a4201ca027
--- /dev/null
+++ b/.rubocop_todo/rspec/factory_bot/local_static_assignment.yml
@@ -0,0 +1,3 @@
+---
+RSpec/FactoryBot/LocalStaticAssignment:
+ Details: grace period
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 84cd2d63778..43c6a2652b9 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-05463be9a1df998a5a02f8b4063bad83040bc649
+da80ab3efcbf3dc0289aacf698ffeabc3c381275
diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
index 3f6ea56382f..035e26d09e6 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -9,6 +9,7 @@ import {
GlIcon,
GlPagination,
GlFormCheckbox,
+ GlTooltipDirective,
} from '@gitlab/ui';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
@@ -43,6 +44,7 @@ import {
I18N_BULK_DELETE_PARTIAL_ERROR,
I18N_BULK_DELETE_CONFIRMATION_TOAST,
SELECTED_ARTIFACTS_MAX_COUNT,
+ I18N_BULK_DELETE_MAX_SELECTED,
} from '../constants';
import JobCheckbox from './job_checkbox.vue';
import ArtifactsBulkDelete from './artifacts_bulk_delete.vue';
@@ -78,6 +80,9 @@ export default {
ArtifactsTableRowDetails,
FeedbackBanner,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
mixins: [glFeatureFlagsMixin()],
inject: ['projectId', 'projectPath', 'canDestroyArtifacts'],
apollo: {
@@ -164,6 +169,25 @@ export default {
artifactsToDelete() {
return this.isDeletingArtifactsForJob ? this.jobArtifactsToDelete : this.selectedArtifacts;
},
+ isAnyVisibleArtifactSelected() {
+ return this.jobArtifacts.some((job) =>
+ job.artifacts.nodes.some((artifactNode) =>
+ this.selectedArtifacts.includes(artifactNode.id),
+ ),
+ );
+ },
+ areAllVisibleArtifactsSelected() {
+ return this.jobArtifacts.every((job) =>
+ job.artifacts.nodes.every((artifactNode) =>
+ this.selectedArtifacts.includes(artifactNode.id),
+ ),
+ );
+ },
+ selectAllTooltipText() {
+ return this.isSelectedArtifactsLimitReached && !this.isAnyVisibleArtifactSelected
+ ? I18N_BULK_DELETE_MAX_SELECTED
+ : '';
+ },
},
methods: {
refetchArtifacts() {
@@ -205,11 +229,11 @@ export default {
}
},
selectArtifact(artifactNode, checked) {
- if (checked) {
- if (!this.isSelectedArtifactsLimitReached) {
- this.selectedArtifacts.push(artifactNode.id);
- }
- } else {
+ const isSelected = this.selectedArtifacts.includes(artifactNode.id);
+
+ if (checked && !isSelected && !this.isSelectedArtifactsLimitReached) {
+ this.selectedArtifacts.push(artifactNode.id);
+ } else if (isSelected) {
this.selectedArtifacts.splice(this.selectedArtifacts.indexOf(artifactNode.id), 1);
}
},
@@ -274,6 +298,11 @@ export default {
this.isBulkDeleteModalVisible = false;
this.jobArtifactsToDelete = [];
},
+ handleSelectAllChecked(checked) {
+ this.jobArtifacts.map((job) =>
+ job.artifacts.nodes.map((artifactNode) => this.selectArtifact(artifactNode, checked)),
+ );
+ },
clearSelectedArtifacts() {
this.selectedArtifacts = [];
},
@@ -369,10 +398,12 @@ export default {
</template>
<template v-if="canBulkDestroyArtifacts" #head(checkbox)>
<gl-form-checkbox
- :disabled="!anyArtifactsSelected"
- :checked="anyArtifactsSelected"
- :indeterminate="anyArtifactsSelected"
- @change="clearSelectedArtifacts"
+ v-gl-tooltip.right
+ :title="selectAllTooltipText"
+ :checked="isAnyVisibleArtifactSelected"
+ :indeterminate="isAnyVisibleArtifactSelected && !areAllVisibleArtifactsSelected"
+ :disabled="isSelectedArtifactsLimitReached && !isAnyVisibleArtifactSelected"
+ @change="handleSelectAllChecked"
/>
</template>
<template
diff --git a/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue b/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue
index 91296bd507e..861278147e9 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue
@@ -48,7 +48,7 @@ export default {
},
},
methods: {
- handleInput(checked) {
+ handleChange(checked) {
if (checked) {
this.unselectedArtifacts.forEach((node) => this.$emit('selectArtifact', node, true));
} else {
@@ -65,6 +65,6 @@ export default {
:disabled="disabled"
:checked="checked"
:indeterminate="indeterminate"
- @input="handleInput"
+ @change="handleChange"
/>
</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
index 3ef73e7c874..b7a612a9688 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
@@ -17,6 +17,10 @@ import {
DELETE_CANDIDATE_PRIMARY_ACTION_LABEL,
DELETE_CANDIDATE_MODAL_TITLE,
MLFLOW_ID_LABEL,
+ CI_SECTION_LABEL,
+ JOB_LABEL,
+ CI_USER_LABEL,
+ CI_MR_LABEL,
} from './translations';
export default {
@@ -43,11 +47,18 @@ export default {
DELETE_CANDIDATE_PRIMARY_ACTION_LABEL,
DELETE_CANDIDATE_MODAL_TITLE,
MLFLOW_ID_LABEL,
+ CI_SECTION_LABEL,
+ JOB_LABEL,
+ CI_USER_LABEL,
+ CI_MR_LABEL,
},
computed: {
info() {
return Object.freeze(this.candidate.info);
},
+ ciJob() {
+ return Object.freeze(this.info.ci_job);
+ },
sections() {
return [
{
@@ -106,6 +117,31 @@ export default {
:text="$options.i18n.ARTIFACTS_LABEL"
/>
+ <template v-if="ciJob">
+ <tr class="divider"></tr>
+
+ <detail-row
+ :label="$options.i18n.JOB_LABEL"
+ :text="ciJob.name"
+ :href="ciJob.path"
+ :section-label="$options.i18n.CI_SECTION_LABEL"
+ />
+
+ <detail-row
+ v-if="ciJob.user"
+ :label="$options.i18n.CI_USER_LABEL"
+ :href="ciJob.user.path"
+ :text="ciJob.user.username"
+ />
+
+ <detail-row
+ v-if="ciJob.merge_request"
+ :label="$options.i18n.CI_MR_LABEL"
+ :text="ciJob.merge_request.title"
+ :href="ciJob.merge_request.path"
+ />
+ </template>
+
<template v-for="{ sectionName, sectionValues } in sections">
<tr v-if="sectionValues" :key="sectionName" class="divider"></tr>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
index 66ee84adb4e..fa9518f3e27 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { __, s__ } from '~/locale';
export const TITLE_LABEL = s__('MlExperimentTracking|Model candidate details');
export const INFO_LABEL = s__('MlExperimentTracking|Info');
@@ -15,3 +15,7 @@ export const DELETE_CANDIDATE_CONFIRMATION_MESSAGE = s__(
);
export const DELETE_CANDIDATE_PRIMARY_ACTION_LABEL = s__('MlExperimentTracking|Delete candidate');
export const DELETE_CANDIDATE_MODAL_TITLE = s__('MLExperimentTracking|Delete candidate?');
+export const CI_SECTION_LABEL = __('CI');
+export const JOB_LABEL = __('Job');
+export const CI_USER_LABEL = s__('MlExperimentTracking|Triggered by');
+export const CI_MR_LABEL = __('Merge request');
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
new file mode 100644
index 00000000000..936938f3032
--- /dev/null
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
@@ -0,0 +1,121 @@
+<script>
+import { createAlert } from '~/alert';
+import commitReferencesQuery from '../graphql/queries/commit_references.query.graphql';
+import containingBranchesQuery from '../graphql/queries/commit_containing_branches.query.graphql';
+import containingTagsQuery from '../graphql/queries/commit_containing_tags.query.graphql';
+import {
+ BRANCHES,
+ TAGS,
+ FETCH_CONTAINING_REFS_EVENT,
+ FETCH_COMMIT_REFERENCES_ERROR,
+} from '../constants';
+import RefsList from './refs_list.vue';
+
+export default {
+ name: 'CommitRefs',
+ components: {
+ RefsList,
+ },
+ inject: ['fullPath', 'commitSha'],
+ apollo: {
+ project: {
+ query: commitReferencesQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update({
+ project: {
+ commitReferences: { tippingTags, tippingBranches, containingBranches, containingTags },
+ },
+ }) {
+ this.tippingTags = tippingTags.names;
+ this.tippingBranches = tippingBranches.names;
+ this.hasContainingBranches = Boolean(containingBranches.names.length);
+ this.hasContainingTags = Boolean(containingTags.names.length);
+ },
+ error() {
+ createAlert({
+ message: this.$options.i18n.errorMessage,
+ captureError: true,
+ });
+ },
+ },
+ },
+ data() {
+ return {
+ containingTags: [],
+ containingBranches: [],
+ tippingTags: [],
+ tippingBranches: [],
+ hasContainingBranches: false,
+ hasContainingTags: false,
+ };
+ },
+ computed: {
+ hasBranches() {
+ return this.tippingBranches.length || this.hasContainingBranches;
+ },
+ hasTags() {
+ return this.tippingTags.length || this.hasContainingTags;
+ },
+ queryVariables() {
+ return {
+ fullPath: this.fullPath,
+ commitSha: this.commitSha,
+ };
+ },
+ },
+ methods: {
+ async fetchContainingRefs({ query, namespace }) {
+ try {
+ const { data } = await this.$apollo.query({
+ query,
+ variables: this.queryVariables,
+ });
+ this[namespace] = data.project.commitReferences[namespace].names;
+ return data.project.commitReferences[namespace].names;
+ } catch {
+ return createAlert({
+ message: this.$options.i18n.errorMessage,
+ captureError: true,
+ });
+ }
+ },
+ fetchContainingBranches() {
+ this.fetchContainingRefs({ query: containingBranchesQuery, namespace: 'containingBranches' });
+ },
+ fetchContainingTags() {
+ this.fetchContainingRefs({ query: containingTagsQuery, namespace: 'containingTags' });
+ },
+ },
+ i18n: {
+ branches: BRANCHES,
+ tags: TAGS,
+ errorMessage: FETCH_COMMIT_REFERENCES_ERROR,
+ },
+ fetchContainingRefsEvent: FETCH_CONTAINING_REFS_EVENT,
+};
+</script>
+
+<template>
+ <div class="gl-ml-7">
+ <refs-list
+ v-if="hasBranches"
+ :has-containing-refs="hasContainingBranches"
+ :is-loading="$apollo.queries.project.loading"
+ :tipping-refs="tippingBranches"
+ :containing-refs="containingBranches"
+ :namespace="$options.i18n.branches"
+ @[$options.fetchContainingRefsEvent]="fetchContainingBranches"
+ />
+ <refs-list
+ v-if="hasTags"
+ :has-containing-refs="hasContainingTags"
+ :is-loading="$apollo.queries.project.loading"
+ :tipping-refs="tippingTags"
+ :containing-refs="containingTags"
+ :namespace="$options.i18n.tags"
+ @[$options.fetchContainingRefsEvent]="fetchContainingTags"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
new file mode 100644
index 00000000000..602fa26efa7
--- /dev/null
+++ b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlCollapse, GlBadge, GlButton, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
+import { CONTAINING_COMMIT, FETCH_CONTAINING_REFS_EVENT } from '../constants';
+
+export default {
+ name: 'RefsList',
+ components: {
+ GlCollapse,
+ GlSkeletonLoader,
+ GlBadge,
+ GlButton,
+ GlIcon,
+ },
+ props: {
+ containingRefs: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ tippingRefs: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ namespace: {
+ type: String,
+ required: true,
+ },
+ hasContainingRefs: {
+ type: Boolean,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isContainingRefsVisible: false,
+ };
+ },
+ computed: {
+ collapseIcon() {
+ return this.isContainingRefsVisible ? 'chevron-down' : 'chevron-right';
+ },
+ isLoadingRefs() {
+ return this.isLoading && !this.containingRefs.length;
+ },
+ },
+ methods: {
+ toggleCollapse() {
+ this.isContainingRefsVisible = !this.isContainingRefsVisible;
+ },
+ showRefs() {
+ this.toggleCollapse();
+ this.$emit(FETCH_CONTAINING_REFS_EVENT);
+ },
+ },
+ i18n: {
+ containingCommit: CONTAINING_COMMIT,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-pt-4">
+ <span data-testid="title" class="gl-mr-2">{{ namespace }}</span>
+ <gl-badge v-for="ref in tippingRefs" :key="ref" class="gl-mt-2 gl-mr-2" size="sm">{{
+ ref
+ }}</gl-badge>
+ <gl-button
+ v-if="hasContainingRefs"
+ class="gl-mr-2 gl-font-sm!"
+ variant="link"
+ size="small"
+ @click="showRefs"
+ >
+ <gl-icon :name="collapseIcon" :size="14" />
+ {{ namespace }} {{ $options.i18n.containingCommit }}
+ </gl-button>
+ <gl-collapse :visible="isContainingRefsVisible">
+ <gl-skeleton-loader v-if="isLoadingRefs" :lines="1" />
+ <template v-else>
+ <gl-badge v-for="ref in containingRefs" :key="ref" class="gl-mt-3 gl-mr-2" size="sm">{{
+ ref
+ }}</gl-badge>
+ </template>
+ </gl-collapse>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/commit_box/info/constants.js b/app/assets/javascripts/projects/commit_box/info/constants.js
index be0bf715314..f255d6c3877 100644
--- a/app/assets/javascripts/projects/commit_box/info/constants.js
+++ b/app/assets/javascripts/projects/commit_box/info/constants.js
@@ -1,7 +1,19 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const COMMIT_BOX_POLL_INTERVAL = 10000;
export const PIPELINE_STATUS_FETCH_ERROR = __(
'There was a problem fetching the latest pipeline status.',
);
+
+export const BRANCHES = s__('Commit|Branches');
+
+export const TAGS = s__('Commit|Tags');
+
+export const CONTAINING_COMMIT = s__('Commit|containing commit');
+
+export const FETCH_CONTAINING_REFS_EVENT = 'fetch-containing-refs';
+
+export const FETCH_COMMIT_REFERENCES_ERROR = s__(
+ 'Commit|There was an error fetching the commit references. Please try again later.',
+);
diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_branches.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_branches.query.graphql
new file mode 100644
index 00000000000..ea74efdbc46
--- /dev/null
+++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_branches.query.graphql
@@ -0,0 +1,10 @@
+query CommitContainingBranches($fullPath: ID!, $commitSha: String!) {
+ project(fullPath: $fullPath) {
+ id
+ commitReferences(commitSha: $commitSha) {
+ containingBranches(excludeTipped: true) {
+ names
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_tags.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_tags.query.graphql
new file mode 100644
index 00000000000..d736dc3ab66
--- /dev/null
+++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_tags.query.graphql
@@ -0,0 +1,10 @@
+query CommitContainingTags($fullPath: ID!, $commitSha: String!) {
+ project(fullPath: $fullPath) {
+ id
+ commitReferences(commitSha: $commitSha) {
+ containingTags(excludeTipped: true) {
+ names
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_references.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_references.query.graphql
new file mode 100644
index 00000000000..71d911c2acc
--- /dev/null
+++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_references.query.graphql
@@ -0,0 +1,19 @@
+query CommitReferences($fullPath: ID!, $commitSha: String!) {
+ project(fullPath: $fullPath) {
+ id
+ commitReferences(commitSha: $commitSha) {
+ containingBranches(excludeTipped: true, limit: 1) {
+ names
+ }
+ containingTags(excludeTipped: true, limit: 1) {
+ names
+ }
+ tippingBranches {
+ names
+ }
+ tippingTags {
+ names
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/projects/commit_box/info/index.js b/app/assets/javascripts/projects/commit_box/info/index.js
index 7c4b76fd62f..8f09c8e1e11 100644
--- a/app/assets/javascripts/projects/commit_box/info/index.js
+++ b/app/assets/javascripts/projects/commit_box/info/index.js
@@ -1,12 +1,10 @@
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
import { initCommitPipelineMiniGraph } from './init_commit_pipeline_mini_graph';
-import { loadBranches } from './load_branches';
import initCommitPipelineStatus from './init_commit_pipeline_status';
+import initCommitReferences from './init_commit_references';
export const initCommitBoxInfo = () => {
// Display commit related branches
- loadBranches();
-
// Related merge requests to this commit
fetchCommitMergeRequests();
@@ -14,4 +12,6 @@ export const initCommitBoxInfo = () => {
initCommitPipelineMiniGraph();
initCommitPipelineStatus();
+
+ initCommitReferences();
};
diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_references.js b/app/assets/javascripts/projects/commit_box/info/init_commit_references.js
new file mode 100644
index 00000000000..c8497187211
--- /dev/null
+++ b/app/assets/javascripts/projects/commit_box/info/init_commit_references.js
@@ -0,0 +1,32 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import CommitBranches from './components/commit_refs.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default (selector = 'js-commit-branches-and-tags') => {
+ const el = document.getElementById(selector);
+
+ if (!el) {
+ return false;
+ }
+
+ const { fullPath, commitSha } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ fullPath,
+ commitSha,
+ },
+ render(createElement) {
+ return createElement(CommitBranches);
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/commit_box/info/load_branches.js b/app/assets/javascripts/projects/commit_box/info/load_branches.js
deleted file mode 100644
index 8333e70b951..00000000000
--- a/app/assets/javascripts/projects/commit_box/info/load_branches.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import axios from 'axios';
-import { sanitize } from '~/lib/dompurify';
-import { __ } from '~/locale';
-import { initDetailsButton } from './init_details_button';
-
-export const loadBranches = (containerSelector = '.js-commit-box-info') => {
- const containerEl = document.querySelector(containerSelector);
- if (!containerEl) {
- return;
- }
-
- const { commitPath } = containerEl.dataset;
- const branchesEl = containerEl.querySelector('.commit-info.branches');
- axios
- .get(commitPath)
- .then(({ data }) => {
- branchesEl.innerHTML = sanitize(data);
-
- initDetailsButton();
- })
- .catch(() => {
- branchesEl.textContent = __('Failed to load branches. Please try again.');
- });
-};
diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb
index e0594247975..556f04e8786 100644
--- a/app/services/alert_management/process_prometheus_alert_service.rb
+++ b/app/services/alert_management/process_prometheus_alert_service.rb
@@ -6,9 +6,10 @@ module AlertManagement
include ::AlertManagement::AlertProcessing
include ::AlertManagement::Responses
- def initialize(project, payload)
+ def initialize(project, payload, integration: nil)
@project = project
@payload = payload
+ @integration = integration
end
def execute
@@ -24,7 +25,7 @@ module AlertManagement
private
- attr_reader :project, :payload
+ attr_reader :project, :payload, :integration
override :incoming_payload
def incoming_payload
@@ -32,6 +33,7 @@ module AlertManagement
Gitlab::AlertManagement::Payload.parse(
project,
payload,
+ integration: integration,
monitoring_tool: Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
)
end
diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb
index 1d24a113e05..f1c093c89b7 100644
--- a/app/services/projects/prometheus/alerts/notify_service.rb
+++ b/app/services/projects/prometheus/alerts/notify_service.rb
@@ -36,7 +36,7 @@ module Projects
truncate_alerts! if max_alerts_exceeded?
- process_prometheus_alerts
+ process_prometheus_alerts(integration)
created
end
@@ -151,10 +151,10 @@ module Projects
ActiveSupport::SecurityUtils.secure_compare(expected, actual)
end
- def process_prometheus_alerts
+ def process_prometheus_alerts(integration)
alerts.map do |alert|
AlertManagement::ProcessPrometheusAlertService
- .new(project, alert)
+ .new(project, alert, integration: integration)
.execute
end
end
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 079e24c6389..c161e1c9d2a 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -29,15 +29,14 @@
%pre.commit-description<
= preserve(markdown_field(@commit, :description))
-.info-well.js-commit-box-info{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) }
+.info-well
.well-segment
.icon-container.commit-icon
= custom_icon("icon_commit")
%span.cgray= n_('parent', 'parents', @commit.parents.count)
- @commit.parents.each do |parent|
= link_to parent.short_id, project_commit_path(@project, parent), class: "commit-sha"
- .commit-info.branches
- = gl_loading_icon(inline: true, css_class: 'gl-vertical-align-middle')
+ #js-commit-branches-and-tags{ data: { full_path: @project.full_path, commit_sha: @commit.short_id } }
.well-segment.merge-request-info
.icon-container
diff --git a/config/feature_flags/development/password_reset_any_verified_email.yml b/config/feature_flags/development/password_reset_any_verified_email.yml
index 9438c6ef414..2b8ee79b7e8 100644
--- a/config/feature_flags/development/password_reset_any_verified_email.yml
+++ b/config/feature_flags/development/password_reset_any_verified_email.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410038
milestone: '16.0'
type: development
group: group::authentication and authorization
-default_enabled: false
+default_enabled: true
diff --git a/db/migrate/20230512141931_add_group_id_to_dependency_list_exports.rb b/db/migrate/20230512141931_add_group_id_to_dependency_list_exports.rb
new file mode 100644
index 00000000000..ed4245694d4
--- /dev/null
+++ b/db/migrate/20230512141931_add_group_id_to_dependency_list_exports.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddGroupIdToDependencyListExports < Gitlab::Database::Migration[2.1]
+ def change
+ add_column :dependency_list_exports, :group_id, :bigint
+ end
+end
diff --git a/db/post_migrate/20230512143000_remove_dependency_list_exports_project_id_not_null_constraint.rb b/db/post_migrate/20230512143000_remove_dependency_list_exports_project_id_not_null_constraint.rb
new file mode 100644
index 00000000000..d7077e9dfdd
--- /dev/null
+++ b/db/post_migrate/20230512143000_remove_dependency_list_exports_project_id_not_null_constraint.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class RemoveDependencyListExportsProjectIdNotNullConstraint < Gitlab::Database::Migration[2.1]
+ def up
+ change_column_null :dependency_list_exports, :project_id, true
+ end
+
+ def down
+ # no-op as there can be null values after the migration
+ end
+end
diff --git a/db/post_migrate/20230515101208_index_group_id_on_dependency_list_exports.rb b/db/post_migrate/20230515101208_index_group_id_on_dependency_list_exports.rb
new file mode 100644
index 00000000000..6019074b1f7
--- /dev/null
+++ b/db/post_migrate/20230515101208_index_group_id_on_dependency_list_exports.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class IndexGroupIdOnDependencyListExports < Gitlab::Database::Migration[2.1]
+ INDEX_NAME = 'index_dependency_list_exports_on_group_id'
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :dependency_list_exports, :group_id, name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :dependency_list_exports, INDEX_NAME
+ end
+end
diff --git a/db/post_migrate/20230515102353_add_foreign_key_to_group_id_on_dependency_list_exports.rb b/db/post_migrate/20230515102353_add_foreign_key_to_group_id_on_dependency_list_exports.rb
new file mode 100644
index 00000000000..2780c551b2d
--- /dev/null
+++ b/db/post_migrate/20230515102353_add_foreign_key_to_group_id_on_dependency_list_exports.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddForeignKeyToGroupIdOnDependencyListExports < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_foreign_key :dependency_list_exports,
+ :namespaces,
+ column: :group_id,
+ on_delete: :cascade,
+ reverse_lock_order: true
+ end
+
+ def down
+ remove_foreign_key_if_exists :dependency_list_exports, column: :group_id
+ end
+end
diff --git a/db/post_migrate/20230517001535_prepare_async_index_for_ci_pipeline_variables_bigint_id.rb b/db/post_migrate/20230517001535_prepare_async_index_for_ci_pipeline_variables_bigint_id.rb
new file mode 100644
index 00000000000..55b46369870
--- /dev/null
+++ b/db/post_migrate/20230517001535_prepare_async_index_for_ci_pipeline_variables_bigint_id.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class PrepareAsyncIndexForCiPipelineVariablesBigintId < Gitlab::Database::Migration[2.1]
+ TABLE_NAME = :ci_pipeline_variables
+ INDEX_NAME = "index_#{TABLE_NAME}_on_id_convert_to_bigint"
+
+ # TODO: Index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/408936
+ def up
+ prepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true, name: INDEX_NAME
+ end
+
+ def down
+ unprepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true, name: INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230512141931 b/db/schema_migrations/20230512141931
new file mode 100644
index 00000000000..7652e281eb7
--- /dev/null
+++ b/db/schema_migrations/20230512141931
@@ -0,0 +1 @@
+2acead22b097b6873efa3108fedb374eeed793aef1519d9825685d97d844d2fe \ No newline at end of file
diff --git a/db/schema_migrations/20230512143000 b/db/schema_migrations/20230512143000
new file mode 100644
index 00000000000..6534634133d
--- /dev/null
+++ b/db/schema_migrations/20230512143000
@@ -0,0 +1 @@
+3a29402e93ec0239bf6012f29b31613f2ea91def3096673ee0b44bfb8624a532 \ No newline at end of file
diff --git a/db/schema_migrations/20230515101208 b/db/schema_migrations/20230515101208
new file mode 100644
index 00000000000..5cd9727331f
--- /dev/null
+++ b/db/schema_migrations/20230515101208
@@ -0,0 +1 @@
+ddd627c22bc925cb186c54d1df2897cba93027c2919dbe279063ac82a496b812 \ No newline at end of file
diff --git a/db/schema_migrations/20230515102353 b/db/schema_migrations/20230515102353
new file mode 100644
index 00000000000..17eee8f1d9f
--- /dev/null
+++ b/db/schema_migrations/20230515102353
@@ -0,0 +1 @@
+c7cad916d89ef08e0c7c184b2c68a09aa5fa7ea39de11e2bff3aa190e74ca986 \ No newline at end of file
diff --git a/db/schema_migrations/20230517001535 b/db/schema_migrations/20230517001535
new file mode 100644
index 00000000000..1511ebfbccb
--- /dev/null
+++ b/db/schema_migrations/20230517001535
@@ -0,0 +1 @@
+5d23a9be6e9ba6424208c3e14fc708e45b3a68e14de7be87e294c4e3d926a31f \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 013b0e67c9a..5631d206fbb 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -15022,11 +15022,12 @@ CREATE TABLE dependency_list_exports (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
- project_id bigint NOT NULL,
+ project_id bigint,
user_id bigint,
file_store integer,
status smallint DEFAULT 0 NOT NULL,
file text,
+ group_id bigint,
CONSTRAINT check_fff6fc9b2f CHECK ((char_length(file) <= 255))
);
@@ -30463,6 +30464,8 @@ CREATE UNIQUE INDEX index_dast_sites_on_project_id_and_url ON dast_sites USING b
CREATE UNIQUE INDEX index_dep_prox_manifests_on_group_id_file_name_and_status ON dependency_proxy_manifests USING btree (group_id, file_name, status);
+CREATE INDEX index_dependency_list_exports_on_group_id ON dependency_list_exports USING btree (group_id);
+
CREATE INDEX index_dependency_list_exports_on_project_id ON dependency_list_exports USING btree (project_id);
CREATE INDEX index_dependency_list_exports_on_user_id ON dependency_list_exports USING btree (user_id);
@@ -35515,6 +35518,9 @@ ALTER TABLE ONLY protected_branches
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_df75a7c8b8 FOREIGN KEY (promoted_to_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
+ALTER TABLE ONLY dependency_list_exports
+ ADD CONSTRAINT fk_e133f6725e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY approval_project_rules
ADD CONSTRAINT fk_e1372c912e FOREIGN KEY (scan_result_policy_id) REFERENCES scan_result_policies(id) ON DELETE CASCADE;
diff --git a/doc/administration/gitaly/troubleshooting.md b/doc/administration/gitaly/troubleshooting.md
index dbbd348556c..ef1f4e06595 100644
--- a/doc/administration/gitaly/troubleshooting.md
+++ b/doc/administration/gitaly/troubleshooting.md
@@ -400,6 +400,10 @@ To resolve this, remove the `noexec` option from the file system mount. An alter
1. Add `gitaly['runtime_dir'] = '<PATH_WITH_EXEC_PERM>'` to `/etc/gitlab/gitlab.rb` and specify a location without `noexec` set.
1. Run `sudo gitlab-ctl reconfigure`.
+### Commit signing fails with `invalid argument: signing key is encrypted` or `invalid data: tag byte does not have MSB set.`
+
+Because Gitaly commit signing is headless and not associated with a specific user, the GPG signing key must be created without a passphrase, or the passphrase must be removed before export.
+
## Troubleshoot Praefect (Gitaly Cluster)
The following sections provide possible solutions to Gitaly Cluster errors.
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
index 0d8c830c016..8d993dfbb63 100644
--- a/doc/integration/azure.md
+++ b/doc/integration/azure.md
@@ -29,9 +29,13 @@ You must set the `uid_field`, which differs across the providers:
| [`omniauth_openid_connect`](https://github.com/omniauth/omniauth_openid_connect/) | `sub` | Specify `uid_field` to use another field |
To migrate from `omniauth-azure-oauth2` to `omniauth_openid_connect` you
-must change the configuration:
+must change the configuration.
-- **For Omnibus installations**
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
+
+Remove some of the existing configuration and add new configuration as shown.
```diff
gitlab_rails['omniauth_providers'] = [
@@ -60,7 +64,9 @@ gitlab_rails['omniauth_providers'] = [
]
```
-- **For installations from source**
+:::TabTitle Self-compiled (source)
+
+Remove some of the existing configuration and add new configuration as shown.
```diff
- { name: 'azure_oauth2',
@@ -88,10 +94,16 @@ gitlab_rails['omniauth_providers'] = [
}
```
+::EndTabs
+
To migrate for example from `omniauth-azure-activedirectory-v2` to `omniauth_openid_connect` you
-must change the configuration:
+must change the configuration.
-- **For Omnibus installations**
+::Tabs
+
+:::TabTitle Linux package (Omnibus)
+
+Remove some of the existing configuration and add new configuration as shown.
```diff
gitlab_rails['omniauth_providers'] = [
@@ -120,7 +132,9 @@ gitlab_rails['omniauth_providers'] = [
]
```
-- **For installations from source**
+:::TabTitle Self-compiled (source)
+
+Remove some of the existing configuration and add new configuration as shown.
```diff
- { name: 'azure_activedirectory_v2',
@@ -148,6 +162,8 @@ gitlab_rails['omniauth_providers'] = [
}
```
+::EndTabs
+
For more information on other customizations, see [`gitlab_username_claim`](index.md#authentication-sources).
## Register an Azure application
diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md
index b5f936e7848..1c6a241d5f4 100644
--- a/doc/user/product_analytics/index.md
+++ b/doc/user/product_analytics/index.md
@@ -23,9 +23,9 @@ For more information, see the [group direction page](https://about.gitlab.com/di
Product analytics uses several tools:
-- [**Jitsu**](https://jitsu.com/docs) - A web and app event collection platform that provides a consistent API to collect user data and pass it through to ClickHouse.
+- [**Snowplow**](https://docs.snowplow.io/docs) - A developer-first engine for collecting behavioral data, and passing it through to ClickHouse.
- [**ClickHouse**](https://clickhouse.com/docs) - A database suited to store, query, and retrieve analytical data.
-- [**Cube.js**](https://cube.dev/docs/) - An analytical graphing library that provides an API to run queries against the data stored in Clickhouse.
+- [**Cube**](https://cube.dev/docs/) - An analytical graphing library that provides an API to run queries against the data stored in Clickhouse.
The following diagram illustrates the product analytics flow:
@@ -34,19 +34,21 @@ The following diagram illustrates the product analytics flow:
title: Product Analytics flow
---
flowchart TB
- subgraph Adding data
- A([SDK]) --Send user data--> B[Analytics Proxy]
- B --Transform data and pass it through--> C[Snowplow]
- C --Pass the data to the associated database--> D([Clickhouse])
+ subgraph Event collection
+ A([SDK]) --Send user data--> B[Snowplow Collector]
+ B --Pass data through--> C[Snowplow Enricher]
end
- subgraph Showing dashboards
- E([Dashboards]) --Generated from the YAML definition--> F[Dashboard]
+ subgraph Data warehouse
+ C --Transform and enrich data--> D([Clickhouse])
+ end
+ subgraph Data visualization with dashboards
+ E([Dashboards]) --Generated from the YAML definition--> F[Panels/Visualizations]
F --Request data--> G[Product Analytics API]
- G --Run Cube queries with pre-aggregations--> H[Cube.js]
+ G --Run Cube queries with pre-aggregations--> H[Cube]
H --Get data from database--> D
D --Return results--> H
- H --> G
- G --Transform data to be rendered--> F
+ H --Transform data to be rendered--> G
+ G --Return data--> F
end
```
@@ -73,20 +75,6 @@ Prerequisite:
1. On the left sidebar, select **Settings > General**.
1. Expand the **Analytics** tab and find the **Product analytics** section.
1. Select **Enable product analytics** and enter the configuration values.
- The following table shows the required configuration parameters and example values:
-
- | Name | Value |
- |--------------------------------|------------------------------------------------------------|
- | Configurator connection string | `https://test:test@configurator.gitlab.com` |
- | Jitsu host | `https://jitsu.gitlab.com` |
- | Jitsu project ID | `g0maofw84gx5sjxgse2k` |
- | Jitsu administrator email | `jitsu.admin@gitlab.com` |
- | Jitsu administrator password | `<your_password>` |
- | Collector host | `https://collector.gitlab.com` |
- | ClickHouse URL | `https://<username>:<password>@clickhouse.gitlab.com:8123` |
- | Cube API URL | `https://cube.gitlab.com` |
- | Cube API key | `25718201b3e9...ae6bbdc62dbb` |
-
1. Select **Save changes**.
## Product analytics dashboards
diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb
index 01dcb95eab5..5b136431ce7 100644
--- a/lib/gitlab/alert_management/payload/base.rb
+++ b/lib/gitlab/alert_management/payload/base.rb
@@ -181,7 +181,6 @@ module Gitlab
end
end
- # Overriden in EE::Gitlab::AlertManagement::Payload::Generic
def value_for_paths(paths)
target_path = paths.find { |path| payload&.dig(*path) }
diff --git a/lib/gitlab/alert_management/payload/prometheus.rb b/lib/gitlab/alert_management/payload/prometheus.rb
index 4c36ebbf3aa..76f3da8366b 100644
--- a/lib/gitlab/alert_management/payload/prometheus.rb
+++ b/lib/gitlab/alert_management/payload/prometheus.rb
@@ -94,6 +94,10 @@ module Gitlab
project && title && starts_at_raw
end
+ def source
+ integration&.name || monitoring_tool
+ end
+
private
override :severity_mapping
@@ -131,3 +135,5 @@ module Gitlab
end
end
end
+
+Gitlab::AlertManagement::Payload::Prometheus.prepend_mod
diff --git a/lib/sidebars/user_profile/panel.rb b/lib/sidebars/user_profile/panel.rb
index 9a595fdf64c..1852ef928f4 100644
--- a/lib/sidebars/user_profile/panel.rb
+++ b/lib/sidebars/user_profile/panel.rb
@@ -4,6 +4,9 @@ module Sidebars
module UserProfile
class Panel < ::Sidebars::Panel
include UsersHelper
+ include Gitlab::Allowable
+
+ delegate :current_user, to: :@context
override :configure_menus
def configure_menus
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 61bb794f7d9..6f2d5b8456a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8383,6 +8383,9 @@ msgstr ""
msgid "CHANGELOG"
msgstr ""
+msgid "CI"
+msgstr ""
+
msgid "CI Lint"
msgstr ""
@@ -11098,6 +11101,18 @@ msgstr ""
msgid "Committed-before"
msgstr ""
+msgid "Commit|Branches"
+msgstr ""
+
+msgid "Commit|Tags"
+msgstr ""
+
+msgid "Commit|There was an error fetching the commit references. Please try again later."
+msgstr ""
+
+msgid "Commit|containing commit"
+msgstr ""
+
msgid "Community forum"
msgstr ""
@@ -28981,6 +28996,9 @@ msgstr ""
msgid "MlExperimentTracking|Status"
msgstr ""
+msgid "MlExperimentTracking|Triggered by"
+msgstr ""
+
msgid "Modal updated"
msgstr ""
diff --git a/rubocop/cop/rspec/factory_bot/local_static_assignment.rb b/rubocop/cop/rspec/factory_bot/local_static_assignment.rb
new file mode 100644
index 00000000000..1a26bc31c78
--- /dev/null
+++ b/rubocop/cop/rspec/factory_bot/local_static_assignment.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'rubocop-rspec'
+
+module RuboCop
+ module Cop
+ module RSpec
+ module FactoryBot
+ # Flags local assignments during factory "load time". This leads to
+ # static data definitions.
+ #
+ # Move these definitions into attribute block or
+ # `transient` block to ensure that the data is evaluated during
+ # "runtime" and remains dynamic.
+ #
+ # @example
+ # # bad
+ # factory :foo do
+ # random = rand(23)
+ # baz { "baz-#{random}" }
+ #
+ # trait :a_trait do
+ # random = rand(23)
+ # baz { "baz-#{random}" }
+ # end
+ #
+ # transient do
+ # random = rand(23)
+ # baz { "baz-#{random}" }
+ # end
+ # end
+ #
+ # # good
+ # factory :foo do
+ # baz { "baz-#{random}" }
+ #
+ # trait :a_trait do
+ # baz { "baz-#{random}" }
+ # end
+ #
+ # transient do
+ # random { rand(23) }
+ # end
+ # end
+ class LocalStaticAssignment < RuboCop::Cop::Base
+ MSG = 'Avoid local static assignemnts in factories which lead to static data definitions.'
+
+ RESTRICT_ON_SEND = %i[factory transient trait].freeze
+
+ def_node_search :local_assignment, <<~PATTERN
+ (begin $(lvasgn ...))
+ PATTERN
+
+ def on_send(node)
+ return unless node.parent&.block_type?
+
+ node.parent.each_child_node(:begin) do |begin_node|
+ begin_node.each_child_node(:lvasgn) do |lvasgn_node|
+ add_offense(lvasgn_node)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
index 926cd7ea681..428ce5b5607 100644
--- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MergeRequests::ConflictsController do
+RSpec.describe Projects::MergeRequests::ConflictsController, feature_category: :code_review_workflow do
let(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
diff --git a/spec/controllers/projects/merge_requests/content_controller_spec.rb b/spec/controllers/projects/merge_requests/content_controller_spec.rb
index 0116071bddf..69edb47fe71 100644
--- a/spec/controllers/projects/merge_requests/content_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/content_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::MergeRequests::ContentController do
+RSpec.describe Projects::MergeRequests::ContentController, feature_category: :code_review_workflow do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
index 6632473a85c..c3a5255b584 100644
--- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Projects::MergeRequests::DraftsController do
+RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :code_review_workflow do
include RepoHelpers
let(:project) { create(:project, :repository) }
diff --git a/spec/factories/design_management/designs.rb b/spec/factories/design_management/designs.rb
index d16fd0c297b..b284c7f5737 100644
--- a/spec/factories/design_management/designs.rb
+++ b/spec/factories/design_management/designs.rb
@@ -26,7 +26,7 @@ FactoryBot.define do
sequence(:relative_position) { |n| n * 1000 }
end
- create_versions = ->(design, evaluator, commit_version) do
+ create_versions = ->(design, evaluator, commit_version) do # rubocop:disable RSpec/FactoryBot/LocalStaticAssignment
unless evaluator.versions_count == 0
project = design.project
issue = design.issue
diff --git a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
index 514644a92f2..046302d07c1 100644
--- a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -79,6 +79,16 @@ describe('JobArtifactsTable component', () => {
const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button');
const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
+ // first checkbox is the "select all" checkbox in the table header
+ const findSelectAllCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findSelectAllCheckboxChecked = () => findSelectAllCheckbox().find('input').element.checked;
+ const findSelectAllCheckboxIndeterminate = () =>
+ findSelectAllCheckbox().find('input').element.indeterminate;
+ const findSelectAllCheckboxDisabled = () =>
+ findSelectAllCheckbox().find('input').element.disabled;
+ const toggleSelectAllCheckbox = () =>
+ findSelectAllCheckbox().vm.$emit('change', !findSelectAllCheckboxChecked());
+
// first checkbox is a "select all", this finder should get the first job checkbox
const findJobCheckbox = (i = 1) => wrapper.findAllComponents(GlFormCheckbox).at(i);
const findAnyCheckbox = () => wrapper.findComponent(GlFormCheckbox);
@@ -125,7 +135,15 @@ describe('JobArtifactsTable component', () => {
},
});
- const maxSelectedArtifacts = new Array(SELECTED_ARTIFACTS_MAX_COUNT).fill({});
+ const allArtifacts = getJobArtifactsResponse.data.project.jobs.nodes
+ .map((jobNode) => jobNode.artifacts.nodes.map((artifactNode) => artifactNode.id))
+ .reduce((artifacts, jobArtifacts) => artifacts.concat(jobArtifacts));
+
+ const maxSelectedArtifacts = new Array(SELECTED_ARTIFACTS_MAX_COUNT).fill('artifact-id');
+ const maxSelectedArtifactsIncludingCurrentPage = [
+ ...allArtifacts,
+ ...new Array(SELECTED_ARTIFACTS_MAX_COUNT - allArtifacts.length).fill('artifact-id'),
+ ];
const createComponent = ({
handlers = {
@@ -394,7 +412,7 @@ describe('JobArtifactsTable component', () => {
it('does not clear selected artifacts on success', async () => {
// select job 2 via checkbox
- findJobCheckbox(2).vm.$emit('input', true);
+ findJobCheckbox(2).vm.$emit('change', true);
// click delete button job 1
findDeleteButton().vm.$emit('click');
@@ -434,7 +452,7 @@ describe('JobArtifactsTable component', () => {
await waitForPromises();
// select job 2 via checkbox
- findJobCheckbox(2).vm.$emit('input', true);
+ findJobCheckbox(2).vm.$emit('change', true);
// click delete button job 1
findDeleteButton().vm.$emit('click');
@@ -494,14 +512,14 @@ describe('JobArtifactsTable component', () => {
it('shows selected artifacts when a job is checked', async () => {
expect(findBulkDeleteContainer().exists()).toBe(false);
- await findJobCheckbox().vm.$emit('input', true);
+ await findJobCheckbox().vm.$emit('change', true);
expect(findBulkDeleteContainer().exists()).toBe(true);
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts);
});
it('disappears when selected artifacts are cleared', async () => {
- await findJobCheckbox().vm.$emit('input', true);
+ await findJobCheckbox().vm.$emit('change', true);
expect(findBulkDeleteContainer().exists()).toBe(true);
@@ -511,7 +529,7 @@ describe('JobArtifactsTable component', () => {
});
it('shows a modal to confirm bulk delete', async () => {
- findJobCheckbox().vm.$emit('input', true);
+ findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
await nextTick();
@@ -520,7 +538,7 @@ describe('JobArtifactsTable component', () => {
});
it('deletes the selected artifacts and shows a toast', async () => {
- findJobCheckbox().vm.$emit('input', true);
+ findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
findBulkDeleteModal().vm.$emit('primary');
@@ -537,7 +555,7 @@ describe('JobArtifactsTable component', () => {
});
it('clears selected artifacts on success', async () => {
- findJobCheckbox().vm.$emit('input', true);
+ findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
findBulkDeleteModal().vm.$emit('primary');
@@ -545,34 +563,216 @@ describe('JobArtifactsTable component', () => {
expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
});
- });
- describe('when the selected artifacts limit is reached', () => {
- beforeEach(async () => {
- createComponent({
- canDestroyArtifacts: true,
- glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
- data: { selectedArtifacts: maxSelectedArtifacts },
+ describe('select all checkbox', () => {
+ describe('when no artifacts are selected', () => {
+ it('is not checked', () => {
+ expect(findSelectAllCheckboxChecked()).toBe(false);
+ expect(findSelectAllCheckboxIndeterminate()).toBe(false);
+ });
+
+ it('selects all artifacts when toggled', async () => {
+ toggleSelectAllCheckbox();
+
+ await nextTick();
+
+ expect(findSelectAllCheckboxChecked()).toBe(true);
+ expect(findSelectAllCheckboxIndeterminate()).toBe(false);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(allArtifacts);
+ });
});
- await nextTick();
+ describe('when some artifacts are selected', () => {
+ beforeEach(async () => {
+ findJobCheckbox().vm.$emit('change', true);
+
+ await nextTick();
+ });
+
+ it('is indeterminate', () => {
+ expect(findSelectAllCheckboxChecked()).toBe(true);
+ expect(findSelectAllCheckboxIndeterminate()).toBe(true);
+ });
+
+ it('deselects all artifacts when toggled', async () => {
+ toggleSelectAllCheckbox();
+
+ await nextTick();
+
+ expect(findSelectAllCheckboxChecked()).toBe(false);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
+ });
+ });
+
+ describe('when all artifacts are selected', () => {
+ beforeEach(async () => {
+ findJobCheckbox(1).vm.$emit('change', true);
+ findJobCheckbox(2).vm.$emit('change', true);
+
+ await nextTick();
+ });
+
+ it('is checked', () => {
+ expect(findSelectAllCheckboxChecked()).toBe(true);
+ expect(findSelectAllCheckboxIndeterminate()).toBe(false);
+ });
+
+ it('deselects all artifacts when toggled', async () => {
+ toggleSelectAllCheckbox();
+
+ await nextTick();
+
+ expect(findSelectAllCheckboxChecked()).toBe(false);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]);
+ });
+ });
+
+ describe('when an artifact is selected on another page', () => {
+ const otherPageArtifact = { id: 'gid://gitlab/Ci::JobArtifact/some/other/id' };
+
+ beforeEach(async () => {
+ // expand the first job row to access the details component
+ findCount().trigger('click');
+
+ await nextTick();
+
+ // mock the selection of an artifact on another page by emitting a select event
+ findDetailsInRow(1).vm.$emit('selectArtifact', otherPageArtifact, true);
+ });
+
+ it('is not checked even though an artifact is selected', () => {
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([
+ otherPageArtifact.id,
+ ]);
+ expect(findSelectAllCheckboxChecked()).toBe(false);
+ expect(findSelectAllCheckboxIndeterminate()).toBe(false);
+ });
+
+ it('only toggles selection of visible artifacts, leaving the other artifact selected', async () => {
+ toggleSelectAllCheckbox();
+
+ await nextTick();
+
+ expect(findSelectAllCheckboxChecked()).toBe(true);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([
+ otherPageArtifact.id,
+ ...allArtifacts,
+ ]);
+
+ toggleSelectAllCheckbox();
+
+ await nextTick();
+
+ expect(findSelectAllCheckboxChecked()).toBe(false);
+ expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([
+ otherPageArtifact.id,
+ ]);
+ });
+ });
});
+ });
+
+ describe('select all checkbox respects selected artifacts limit', () => {
+ describe('when selecting all visible artifacts would exceed the limit', () => {
+ const selectedArtifactsLength = SELECTED_ARTIFACTS_MAX_COUNT - 1;
+
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ data: {
+ selectedArtifacts: new Array(selectedArtifactsLength).fill('artifact-id'),
+ },
+ });
+
+ await nextTick();
+ });
+
+ it('selects only up to the limit', async () => {
+ expect(findSelectAllCheckboxChecked()).toBe(false);
+ expect(findBulkDelete().props('selectedArtifacts')).toHaveLength(selectedArtifactsLength);
- it('passes isSelectedArtifactsLimitReached to bulk delete', () => {
- expect(findBulkDelete().props('isSelectedArtifactsLimitReached')).toBe(true);
+ toggleSelectAllCheckbox();
+
+ await nextTick();
+
+ expect(findSelectAllCheckboxChecked()).toBe(true);
+ expect(findBulkDelete().props('selectedArtifacts')).toHaveLength(
+ SELECTED_ARTIFACTS_MAX_COUNT,
+ );
+ expect(findBulkDelete().props('selectedArtifacts')).not.toContain(
+ allArtifacts[allArtifacts.length - 1],
+ );
+ });
});
- it('passes isSelectedArtifactsLimitReached to job checkbox', () => {
- expect(wrapper.findComponent(JobCheckbox).props('isSelectedArtifactsLimitReached')).toBe(
- true,
- );
+ describe('when limit has been reached without artifacts on the current page', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ data: { selectedArtifacts: maxSelectedArtifacts },
+ });
+
+ await nextTick();
+ });
+
+ it('passes isSelectedArtifactsLimitReached to bulk delete', () => {
+ expect(findBulkDelete().props('isSelectedArtifactsLimitReached')).toBe(true);
+ });
+
+ it('passes isSelectedArtifactsLimitReached to job checkbox', () => {
+ expect(wrapper.findComponent(JobCheckbox).props('isSelectedArtifactsLimitReached')).toBe(
+ true,
+ );
+ });
+
+ it('passes isSelectedArtifactsLimitReached to table row details', async () => {
+ findCount().trigger('click');
+ await nextTick();
+
+ expect(findDetailsInRow(1).props('isSelectedArtifactsLimitReached')).toBe(true);
+ });
+
+ it('disables the select all checkbox', () => {
+ expect(findSelectAllCheckboxDisabled()).toBe(true);
+ });
});
- it('passes isSelectedArtifactsLimitReached to table row details', async () => {
- findCount().trigger('click');
- await nextTick();
+ describe('when limit has been reached including artifacts on the current page', () => {
+ beforeEach(async () => {
+ createComponent({
+ canDestroyArtifacts: true,
+ glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true },
+ data: {
+ selectedArtifacts: maxSelectedArtifactsIncludingCurrentPage,
+ },
+ });
+
+ await nextTick();
+ });
+
+ describe('the select all checkbox', () => {
+ it('is checked', () => {
+ expect(findSelectAllCheckboxChecked()).toBe(true);
+ expect(findSelectAllCheckboxIndeterminate()).toBe(false);
+ });
- expect(findDetailsInRow(1).props('isSelectedArtifactsLimitReached')).toBe(true);
+ it('deselects all artifacts when toggled', async () => {
+ expect(findBulkDelete().props('selectedArtifacts')).toHaveLength(
+ SELECTED_ARTIFACTS_MAX_COUNT,
+ );
+
+ toggleSelectAllCheckbox();
+
+ await nextTick();
+
+ expect(findSelectAllCheckboxChecked()).toBe(false);
+ expect(findBulkDelete().props('selectedArtifacts')).toHaveLength(
+ SELECTED_ARTIFACTS_MAX_COUNT - allArtifacts.length,
+ );
+ });
+ });
});
});
@@ -588,7 +788,7 @@ describe('JobArtifactsTable component', () => {
await waitForPromises();
- findJobCheckbox().vm.$emit('input', true);
+ findJobCheckbox().vm.$emit('change', true);
findBulkDelete().vm.$emit('showBulkDeleteModal');
findBulkDeleteModal().vm.$emit('primary');
diff --git a/spec/frontend/ci/artifacts/components/job_checkbox_spec.js b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
index 8b47571239c..73a49506564 100644
--- a/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js
@@ -48,7 +48,7 @@ describe('JobCheckbox component', () => {
});
it('selects the unselected artifacts on click', () => {
- findCheckbox().vm.$emit('input', true);
+ findCheckbox().vm.$emit('change', true);
expect(wrapper.emitted('selectArtifact')).toMatchObject([
[mockUnselectedArtifacts[0], true],
@@ -83,7 +83,7 @@ describe('JobCheckbox component', () => {
});
it('deselects the selected artifacts on click', () => {
- findCheckbox().vm.$emit('input', false);
+ findCheckbox().vm.$emit('change', false);
expect(wrapper.emitted('selectArtifact')).toMatchObject([
[mockSelectedArtifacts[0], false],
@@ -105,7 +105,7 @@ describe('JobCheckbox component', () => {
});
it('selects the artifacts on click', () => {
- findCheckbox().vm.$emit('input', true);
+ findCheckbox().vm.$emit('change', true);
expect(wrapper.emitted('selectArtifact')).toMatchObject([
[mockUnselectedArtifacts[0], true],
diff --git a/spec/frontend/commit/components/commit_refs_spec.js b/spec/frontend/commit/components/commit_refs_spec.js
new file mode 100644
index 00000000000..7c35ff1969c
--- /dev/null
+++ b/spec/frontend/commit/components/commit_refs_spec.js
@@ -0,0 +1,82 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { createAlert } from '~/alert';
+import commitReferences from '~/projects/commit_box/info/graphql/queries/commit_references.query.graphql';
+import containingBranchesQuery from '~/projects/commit_box/info/graphql/queries/commit_containing_branches.query.graphql';
+import RefsList from '~/projects/commit_box/info/components/refs_list.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ FETCH_CONTAINING_REFS_EVENT,
+ FETCH_COMMIT_REFERENCES_ERROR,
+} from '~/projects/commit_box/info/constants';
+import CommitRefs from '~/projects/commit_box/info/components/commit_refs.vue';
+
+import {
+ mockCommitReferencesResponse,
+ mockOnlyBranchesResponse,
+ mockContainingBranchesResponse,
+ refsListPropsMock,
+} from '../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+
+describe('Commit references component', () => {
+ let wrapper;
+
+ const successQueryHandler = (mockResponse) => jest.fn().mockResolvedValue(mockResponse);
+ const failedQueryHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ const containingBranchesQueryHandler = successQueryHandler(mockContainingBranchesResponse);
+ const findRefsLists = () => wrapper.findAllComponents(RefsList);
+ const branchesList = () => findRefsLists().at(0);
+
+ const createComponent = async (
+ commitReferencesQueryHandler = successQueryHandler(mockCommitReferencesResponse),
+ ) => {
+ wrapper = shallowMount(CommitRefs, {
+ apolloProvider: createMockApollo([
+ [commitReferences, commitReferencesQueryHandler],
+ [containingBranchesQuery, containingBranchesQueryHandler],
+ ]),
+ provide: {
+ fullPath: '/some/project',
+ commitSha: 'xxx',
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ it('renders component correcrly', async () => {
+ await createComponent();
+ expect(findRefsLists()).toHaveLength(2);
+ });
+
+ it('passes props to refs list', async () => {
+ await createComponent();
+ expect(branchesList().props()).toEqual(refsListPropsMock);
+ });
+
+ it('shows alert when response fails', async () => {
+ await createComponent(failedQueryHandler);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FETCH_COMMIT_REFERENCES_ERROR,
+ captureError: true,
+ });
+ });
+
+ it('fetches containing refs on the fetch event', async () => {
+ await createComponent();
+ branchesList().vm.$emit(FETCH_CONTAINING_REFS_EVENT);
+ await waitForPromises();
+ expect(containingBranchesQueryHandler).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not render list when there is no branches or tags', async () => {
+ await createComponent(successQueryHandler(mockOnlyBranchesResponse));
+ expect(findRefsLists()).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/commit/components/refs_list_spec.js b/spec/frontend/commit/components/refs_list_spec.js
new file mode 100644
index 00000000000..d124f4a41b8
--- /dev/null
+++ b/spec/frontend/commit/components/refs_list_spec.js
@@ -0,0 +1,70 @@
+import { GlCollapse, GlButton, GlBadge, GlSkeletonLoader } from '@gitlab/ui';
+import RefsList from '~/projects/commit_box/info/components/refs_list.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ CONTAINING_COMMIT,
+ FETCH_CONTAINING_REFS_EVENT,
+} from '~/projects/commit_box/info/constants';
+import { refsListPropsMock, containingBranchesMock } from '../mock_data';
+
+describe('Commit references component', () => {
+ let wrapper;
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(RefsList, {
+ propsData: {
+ ...refsListPropsMock,
+ ...props,
+ },
+ });
+ };
+
+ const findTitle = () => wrapper.findByTestId('title');
+ const findCollapseButton = () => wrapper.findComponent(GlButton);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findTippingRefs = () => wrapper.findAllComponents(GlBadge);
+ const findContainingRefs = () => wrapper.findComponent(GlCollapse);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the namespace passed', () => {
+ expect(findTitle().text()).toEqual(refsListPropsMock.namespace);
+ });
+
+ it('renders list of tipping branches or tags', () => {
+ expect(findTippingRefs()).toHaveLength(refsListPropsMock.tippingRefs.length);
+ });
+
+ it('does not render collapse with containing branches ot tags when there is no data', () => {
+ createComponent({ hasContainingRefs: false });
+ expect(findCollapseButton().exists()).toBe(false);
+ });
+
+ it('renders collapse component if commit has containing branches', () => {
+ expect(findCollapseButton().text()).toContain(CONTAINING_COMMIT);
+ });
+
+ it('Emits event when collapse button is clicked', () => {
+ findCollapseButton().vm.$emit('click');
+ expect(wrapper.emitted()[FETCH_CONTAINING_REFS_EVENT]).toHaveLength(1);
+ });
+
+ it('Renders the list of containing branches or tags when collapse is expanded', () => {
+ createComponent({ containingRefs: containingBranchesMock });
+ const containingRefsList = findContainingRefs();
+ expect(containingRefsList.findAllComponents(GlBadge)).toHaveLength(
+ containingBranchesMock.length,
+ );
+ });
+
+ it('Does not reneder list of tipping branches or tags if there is no data', () => {
+ createComponent({ tippingRefs: [] });
+ expect(findTippingRefs().exists()).toBe(false);
+ });
+
+ it('Renders skeleton loader when isLoading prop has true value', () => {
+ createComponent({ isLoading: true, containingRefs: [] });
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index 3b6971d9607..1c2bcb17663 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -232,3 +232,60 @@ export const x509CertificateDetailsProp = {
subject: 'CN=gitlab@example.org,OU=Example,O=World',
subjectKeyIdentifier: 'BC BC BC BC BC BC BC BC',
};
+
+export const tippingBranchesMock = ['main', 'development'];
+
+export const containingBranchesMock = ['branch-1', 'branch-2', 'branch-3'];
+
+export const mockCommitReferencesResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ commitReferences: {
+ containingBranches: { names: ['branch-1'], __typename: 'CommitParentNames' },
+ containingTags: { names: ['tag-1'], __typename: 'CommitParentNames' },
+ tippingBranches: { names: tippingBranchesMock, __typename: 'CommitParentNames' },
+ tippingTags: { names: ['tag-latest'], __typename: 'CommitParentNames' },
+ __typename: 'CommitReferences',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockOnlyBranchesResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ commitReferences: {
+ containingBranches: { names: ['branch-1'], __typename: 'CommitParentNames' },
+ containingTags: { names: [], __typename: 'CommitParentNames' },
+ tippingBranches: { names: tippingBranchesMock, __typename: 'CommitParentNames' },
+ tippingTags: { names: [], __typename: 'CommitParentNames' },
+ __typename: 'CommitReferences',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockContainingBranchesResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ commitReferences: {
+ containingBranches: { names: containingBranchesMock, __typename: 'CommitParentNames' },
+ __typename: 'CommitReferences',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const refsListPropsMock = {
+ hasContainingRefs: true,
+ containingRefs: [],
+ namespace: 'Branches',
+ tippingRefs: tippingBranchesMock,
+ isLoading: false,
+};
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
index 9d1c22faa8f..07d501c4e44 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
@@ -48,6 +48,14 @@ describe('MlCandidatesShow', () => {
['', 'Status', CANDIDATE.info.status, ''],
['', 'Experiment', CANDIDATE.info.experiment_name, CANDIDATE.info.path_to_experiment],
['', 'Artifacts', 'Artifacts', CANDIDATE.info.path_to_artifact],
+ ['CI', 'Job', CANDIDATE.info.ci_job.name, CANDIDATE.info.ci_job.path],
+ ['', 'Triggered by', CANDIDATE.info.ci_job.user.username, CANDIDATE.info.ci_job.user.path],
+ [
+ '',
+ 'Merge request',
+ CANDIDATE.info.ci_job.merge_request.title,
+ CANDIDATE.info.ci_job.merge_request.path,
+ ],
['Parameters', CANDIDATE.params[0].name, CANDIDATE.params[0].value, ''],
['', CANDIDATE.params[1].name, CANDIDATE.params[1].value, ''],
['Metrics', CANDIDATE.metrics[0].name, CANDIDATE.metrics[0].value, ''],
@@ -75,6 +83,9 @@ describe('MlCandidatesShow', () => {
expect(findSectionLabel('Parameters').exists()).toBe(true);
expect(findSectionLabel('Metadata').exists()).toBe(true);
expect(findSectionLabel('Metrics').exists()).toBe(true);
+ expect(findSectionLabel('CI').exists()).toBe(true);
+ expect(findLabel('Merge request').exists()).toBe(true);
+ expect(findLabel('Triggered by').exists()).toBe(true);
});
});
@@ -99,6 +110,7 @@ describe('MlCandidatesShow', () => {
delete candidate.params;
delete candidate.metrics;
delete candidate.metadata;
+ delete candidate.info.ci_job;
return candidate;
}),
);
@@ -114,6 +126,29 @@ describe('MlCandidatesShow', () => {
it('does not render metrics', () => {
expect(findSectionLabel('Metrics').exists()).toBe(false);
});
+
+ it('does not render CI info', () => {
+ expect(findSectionLabel('CI').exists()).toBe(false);
+ });
+ });
+
+ describe('Has CI, but no user or mr', () => {
+ beforeEach(() =>
+ createWrapper(() => {
+ const candidate = newCandidate();
+ delete candidate.info.ci_job.user;
+ delete candidate.info.ci_job.merge_request;
+ return candidate;
+ }),
+ );
+
+ it('does not render MR info', () => {
+ expect(findLabel('Merge request').exists()).toBe(false);
+ });
+
+ it('does not render CI user info', () => {
+ expect(findLabel('Triggered by').exists()).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
index cad2c03fc93..16c1b29f7bc 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
@@ -19,5 +19,17 @@ export const newCandidate = () => ({
path_to_experiment: 'path/to/experiment',
status: 'SUCCESS',
path: 'path_to_candidate',
+ ci_job: {
+ name: 'test',
+ path: 'path/to/job',
+ merge_request: {
+ path: 'path/to/mr',
+ title: 'Some MR',
+ },
+ user: {
+ path: 'path/to/ci/user',
+ username: 'ciuser',
+ },
+ },
},
});
diff --git a/spec/frontend/projects/commit_box/info/load_branches_spec.js b/spec/frontend/projects/commit_box/info/load_branches_spec.js
deleted file mode 100644
index b00a6378e07..00000000000
--- a/spec/frontend/projects/commit_box/info/load_branches_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-import { setHTMLFixture } from 'helpers/fixtures';
-import waitForPromises from 'helpers/wait_for_promises';
-import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import { loadBranches } from '~/projects/commit_box/info/load_branches';
-import { initDetailsButton } from '~/projects/commit_box/info/init_details_button';
-
-jest.mock('~/projects/commit_box/info/init_details_button');
-
-const mockCommitPath = '/commit/abcd/branches';
-const mockBranchesRes =
- '<a href="/-/commits/main">main</a><span><a href="/-/commits/my-branch">my-branch</a></span>';
-
-describe('~/projects/commit_box/info/load_branches', () => {
- let mock;
-
- const getElInnerHtml = () => document.querySelector('.js-commit-box-info').innerHTML;
-
- beforeEach(() => {
- setHTMLFixture(`
- <div class="js-commit-box-info" data-commit-path="${mockCommitPath}">
- <div class="commit-info branches">
- <span class="spinner"/>
- </div>
- </div>`);
-
- mock = new MockAdapter(axios);
- mock.onGet(mockCommitPath).reply(HTTP_STATUS_OK, mockBranchesRes);
- });
-
- it('initializes the details button', async () => {
- loadBranches();
- await waitForPromises();
-
- expect(initDetailsButton).toHaveBeenCalled();
- });
-
- it('loads and renders branches info', async () => {
- loadBranches();
- await waitForPromises();
-
- expect(getElInnerHtml()).toMatchInterpolatedText(
- `<div class="commit-info branches">${mockBranchesRes}</div>`,
- );
- });
-
- it('does not load when no container is provided', async () => {
- loadBranches('.js-another-class');
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(0);
- });
-
- describe('when branches request returns unsafe content', () => {
- beforeEach(() => {
- mock
- .onGet(mockCommitPath)
- .reply(HTTP_STATUS_OK, '<a onload="alert(\'xss!\');" href="/-/commits/main">main</a>');
- });
-
- it('displays sanitized html', async () => {
- loadBranches();
- await waitForPromises();
-
- expect(getElInnerHtml()).toMatchInterpolatedText(
- '<div class="commit-info branches"><a href="/-/commits/main">main</a></div>',
- );
- });
- });
-
- describe('when branches request fails', () => {
- beforeEach(() => {
- mock.onGet(mockCommitPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Error!');
- });
-
- it('attempts to load and renders an error', async () => {
- loadBranches();
- await waitForPromises();
-
- expect(getElInnerHtml()).toMatchInterpolatedText(
- '<div class="commit-info branches">Failed to load branches. Please try again.</div>',
- );
- });
- });
-});
diff --git a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
index 6a4f35c01e3..8ead292c27a 100644
--- a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
+++ b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb
@@ -297,4 +297,18 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do
it { is_expected.to be_nil }
end
end
+
+ describe '#source' do
+ subject { parsed_payload.source }
+
+ it { is_expected.to eq('Prometheus') }
+
+ context 'with alerting integration provided' do
+ before do
+ parsed_payload.integration = instance_double('::AlertManagement::HttpIntegration', name: 'INTEGRATION')
+ end
+
+ it { is_expected.to eq('INTEGRATION') }
+ end
+ end
end
diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb
index 6414b1efe6a..d1a0ad57a84 100644
--- a/spec/requests/api/internal/base_spec.rb
+++ b/spec/requests/api/internal/base_spec.rb
@@ -513,24 +513,63 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
project.add_developer(user)
end
+ shared_context 'with env passed as a JSON' do
+ let(:obj_dir_relative) { './objects' }
+ let(:alt_obj_dirs_relative) { ['./alt-objects-1', './alt-objects-2'] }
+ let(:env) do
+ {
+ GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
+ GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
+ }
+ end
+ end
+
shared_examples 'sets hook env' do
- context 'with env passed as a JSON' do
- let(:obj_dir_relative) { './objects' }
- let(:alt_obj_dirs_relative) { ['./alt-objects-1', './alt-objects-2'] }
- let(:env) do
- {
- GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
- GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
- }
- end
+ include_context 'with env passed as a JSON'
- it 'sets env in RequestStore' do
- expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys)
+ it 'sets env in RequestStore' do
+ expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys)
- subject
+ subject
- expect(response).to have_gitlab_http_status(:ok)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ shared_examples 'sets hook env and routes to primary' do
+ include_context 'with env passed as a JSON'
+
+ let(:interceptor) do
+ Class.new(::GRPC::ClientInterceptor) do
+ def route_to_primary_received?
+ @route_to_primary_count.to_i > 0
+ end
+
+ def request_response(request:, call:, method:, metadata:) # rubocop:disable Lint/UnusedMethodArgument
+ @route_to_primary_count ||= 0
+ @route_to_primary_count += 1 if metadata['gitaly-route-repository-accessor-policy'] == 'primary-only'
+
+ yield
+ end
+ end.new
+ end
+
+ before do
+ Gitlab::GitalyClient.clear_stubs!
+ allow(::Gitlab::GitalyClient).to receive(:interceptors).and_return([interceptor])
+ end
+
+ after do
+ Gitlab::GitalyClient.clear_stubs!
+ end
+
+ it 'sets env in RequestStore and routes gRPC messages to primary', :request_store do
+ expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys).and_call_original
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(interceptor.route_to_primary_received?).to be_truthy
end
end
@@ -549,6 +588,8 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
expect(user.reload.last_activity_on).to eql(Date.today)
end
+ # Wiki repositories don't invoke any Gitaly RPCs to check for changes, so we can only test for the
+ # hook environment being set.
it_behaves_like 'sets hook env' do
let(:gl_repository) { Gitlab::GlRepository::WIKI.identifier_for_container(project.wiki) }
end
@@ -588,7 +629,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
expect(user.reload.last_activity_on).to eql(Date.today)
end
- it_behaves_like 'sets hook env' do
+ it_behaves_like 'sets hook env and routes to primary' do
let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(personal_snippet) }
end
end
@@ -620,7 +661,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do
expect(user.reload.last_activity_on).to eql(Date.today)
end
- it_behaves_like 'sets hook env' do
+ it_behaves_like 'sets hook env and routes to primary' do
let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(project_snippet) }
end
end
diff --git a/spec/rubocop/cop/rspec/factory_bot/local_static_assignment_spec.rb b/spec/rubocop/cop/rspec/factory_bot/local_static_assignment_spec.rb
new file mode 100644
index 00000000000..de86435616c
--- /dev/null
+++ b/spec/rubocop/cop/rspec/factory_bot/local_static_assignment_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../../../rubocop/cop/rspec/factory_bot/local_static_assignment'
+
+RSpec.describe RuboCop::Cop::RSpec::FactoryBot::LocalStaticAssignment, feature_category: :tooling do
+ shared_examples 'local static assignment' do |block|
+ it "flags static local assignment in `#{block}`" do
+ expect_offense(<<~RUBY, block: block)
+ %{block} do
+ age
+ name
+
+ random_number = rand(23)
+ ^^^^^^^^^^^^^^^^^^^^^^^^ Avoid local static assignemnts in factories which lead to static data definitions.
+
+ random_string = SecureRandom.uuid
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid local static assignemnts in factories which lead to static data definitions.
+
+ project
+ end
+ RUBY
+ end
+
+ it 'does not flag correct use' do
+ expect_no_offenses(<<~RUBY)
+ #{block} do
+ age do
+ random_number = rand(23)
+ random_number + 1
+ end
+ end
+ RUBY
+ end
+ end
+
+ it_behaves_like 'local static assignment', 'factory :project'
+ it_behaves_like 'local static assignment', 'transient'
+ it_behaves_like 'local static assignment', 'trait :closed'
+
+ it 'does not flag local assignments in unrelated blocks' do
+ expect_no_offenses(<<~RUBY)
+ factory :project do
+ sequence(:number) do |n|
+ random_number = rand(23)
+ random_number * n
+ end
+
+ name do
+ random_string = SecureRandom.uuid
+ random_string + "-name"
+ end
+
+ initialize_with do
+ random_string = SecureRandom.uuid
+ new(name: random_string)
+ end
+ end
+ RUBY
+ end
+end
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index 24affa45aa5..cc1f83ddc2b 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -224,12 +224,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService, feature_category: :i
end
context 'process Alert Management alerts' do
- let(:process_service) { instance_double(AlertManagement::ProcessPrometheusAlertService) }
+ let(:integration) { build_stubbed(:alert_management_http_integration, project: project, token: token) }
- before do
- create(:prometheus_integration, project: project)
- create(:project_alerting_setting, project: project, token: token)
- end
+ subject { service.execute(token_input, integration) }
context 'with multiple firing alerts and resolving alerts' do
let(:payload_raw) do
@@ -239,7 +236,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService, feature_category: :i
it 'processes Prometheus alerts' do
expect(AlertManagement::ProcessPrometheusAlertService)
.to receive(:new)
- .with(project, kind_of(Hash))
+ .with(project, kind_of(Hash), integration: integration)
.exactly(3).times
.and_call_original