Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-20 12:09:31 +00:00
parent 55e6eebd6f
commit b70394d26f
83 changed files with 750 additions and 800 deletions

View File

@ -298,9 +298,13 @@ coverage-frontend:
- *yarn-install
- run_timed_command "retry yarn run webpack-prod"
qa-frontend-node:10:
qa-frontend-node:12:
extends: .qa-frontend-node
image: ${GITLAB_DEPENDENCY_PROXY}node:dubnium
image: ${GITLAB_DEPENDENCY_PROXY}node:12
qa-frontend-node:14:
extends: .qa-frontend-node
image: ${GITLAB_DEPENDENCY_PROXY}node:14
qa-frontend-node:latest:
extends:

View File

@ -2548,8 +2548,6 @@ Gitlab/FeatureAvailableUsage:
- 'ee/app/views/projects/settings/operations/_status_page.html.haml'
- 'ee/app/views/projects/settings/repository/_protected_branches.html.haml'
- 'ee/app/views/projects/sidebar/_repository_locked_files.html.haml'
- 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'
- 'ee/app/views/shared/issuable/_board_create_list_dropdown.html.haml'
- 'ee/app/views/shared/issuable/_group_bulk_update_sidebar.html.haml'
- 'ee/app/views/shared/issuable/form/_default_templates.html.haml'
- 'ee/app/views/shared/labels/_create_label_help_text.html.haml'

View File

@ -1 +1 @@
c0bef09e1ad59c7d119ecc5d58a8a4e6e98a8a65
48d7984d9912c935a2c2abba3b55593cf0be2d8e

View File

@ -1,119 +0,0 @@
/* eslint-disable func-names, no-new */
import $ from 'jquery';
import store from '~/boards/stores';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import CreateLabelDropdown from '../../create_label';
import { fullLabelId } from '../boards_util';
import boardsStore from '../stores/boards_store';
function shouldCreateListGraphQL(label) {
return store.getters.shouldUseGraphQL && !store.getters.getListByLabelId(fullLabelId(label));
}
// eslint-disable-next-line @gitlab/no-global-event-off
$(document)
.off('created.label')
.on('created.label', (e, label, addNewList) => {
if (!addNewList) {
return;
}
if (shouldCreateListGraphQL(label)) {
store.dispatch('createList', { labelId: fullLabelId(label) });
} else {
boardsStore.new({
title: label.title,
position: boardsStore.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color,
},
});
}
});
export default function initNewListDropdown() {
$('.js-new-board-list').each(function () {
const $dropdownToggle = $(this);
const $dropdown = $dropdownToggle.closest('.dropdown');
new CreateLabelDropdown(
$dropdown.find('.dropdown-new-label'),
$dropdownToggle.data('namespacePath'),
$dropdownToggle.data('projectPath'),
);
initDeprecatedJQueryDropdown($dropdownToggle, {
data(term, callback) {
const reqFailed = () => {
$dropdownToggle.data('bs.dropdown').hide();
createFlash({
message: __('Error fetching labels.'),
});
};
if (store.getters.shouldUseGraphQL) {
store
.dispatch('fetchLabels')
.then((data) => callback(data))
.catch(reqFailed);
} else {
axios
.get($dropdownToggle.attr('data-list-labels-path'))
.then(({ data }) => callback(data))
.catch(reqFailed);
}
},
renderRow(label) {
const active = store.getters.shouldUseGraphQL
? store.getters.getListByLabelId(label.id)
: boardsStore.findListByLabelId(label.id);
const $li = $('<li />');
const $a = $('<a />', {
class: active ? `is-active js-board-list-${getIdFromGraphQLId(active.id)}` : '',
text: label.title,
href: '#',
});
const $labelColor = $('<span />', {
class: 'dropdown-label-box',
style: `background-color: ${label.color}`,
});
return $li.append($a.prepend($labelColor));
},
search: {
fields: ['title'],
},
filterable: true,
selectable: true,
multiSelect: true,
containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content',
clicked(options) {
const { e } = options;
const label = options.selectedObj;
e.preventDefault();
if (shouldCreateListGraphQL(label)) {
store.dispatch('createList', { labelId: label.id });
} else if (!boardsStore.findListByLabelId(label.id)) {
boardsStore.new({
title: label.title,
position: boardsStore.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color,
},
});
}
},
});
});
}

View File

@ -7,12 +7,7 @@ import { mapActions, mapGetters } from 'vuex';
import 'ee_else_ce/boards/models/issue';
import 'ee_else_ce/boards/models/list';
import BoardSidebar from 'ee_else_ce/boards/components/board_sidebar';
import initNewListDropdown from 'ee_else_ce/boards/components/new_list_dropdown';
import {
setWeightFetchingState,
setEpicFetchingState,
getMilestoneTitle,
} from 'ee_else_ce/boards/ee_functions';
import { setWeightFetchingState, setEpicFetchingState } from 'ee_else_ce/boards/ee_functions';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
@ -313,20 +308,6 @@ export default () => {
},
});
// eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler
new Vue({
el: document.getElementById('js-add-list'),
data() {
return {
filters: boardsStore.state.filters,
...getMilestoneTitle($boardApp),
};
},
mounted() {
initNewListDropdown();
},
});
const createColumnTriggerEl = document.querySelector('.js-create-column-trigger');
if (createColumnTriggerEl) {
// eslint-disable-next-line no-new

View File

@ -10,7 +10,6 @@ import {
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
import updateCurrentBranchMutation from '../../graphql/mutations/update_current_branch.mutation.graphql';
import updateLastCommitBranchMutation from '../../graphql/mutations/update_last_commit_branch.mutation.graphql';
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from '../../graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from '../../graphql/queries/client/is_new_ci_config_file.graphql';
import getPipelineEtag from '../../graphql/queries/client/pipeline_etag.graphql';
@ -37,6 +36,11 @@ export default {
type: String,
required: true,
},
commitSha: {
type: String,
required: false,
default: '',
},
},
data() {
return {
@ -49,9 +53,6 @@ export default {
isNewCiConfigFile: {
query: getIsNewCiConfigFile,
},
commitSha: {
query: getCommitSha,
},
currentBranch: {
query: getCurrentBranch,
},
@ -96,13 +97,7 @@ export default {
lastCommitId: this.commitSha,
},
update(store, { data }) {
const commitSha = data?.commitCreate?.commit?.sha;
const pipelineEtag = data?.commitCreate?.commit?.commitPipelinePath;
if (commitSha) {
store.writeQuery({ query: getCommitSha, data: { commitSha } });
}
if (pipelineEtag) {
store.writeQuery({ query: getPipelineEtag, data: { pipelineEtag } });
}

View File

@ -3,7 +3,6 @@ import { EDITOR_READY_EVENT } from '~/editor/constants';
import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
export default {
components: {
@ -12,14 +11,11 @@ export default {
mixins: [glFeatureFlagMixin()],
inject: ['ciConfigPath', 'projectPath', 'projectNamespace', 'defaultBranch'],
inheritAttrs: false,
data() {
return {
commitSha: '',
};
},
apollo: {
props: {
commitSha: {
query: getCommitSha,
type: String,
required: false,
default: '',
},
},
methods: {

View File

@ -158,11 +158,9 @@ export default {
const updatedPath = setUrlParams({ branch_name: newBranch });
historyPushState(updatedPath);
this.$emit('updateCommitSha', { newBranch });
// refetching the content will cause a lot of components to re-render,
// including the text editor which uses the commit sha to register the CI schema
// so we need to make sure the commit sha is updated first
// so we need to make sure the currentBranch (and consequently, the commitSha) are updated first
await this.$nextTick();
this.$emit('refetchContent');
},

View File

@ -33,6 +33,11 @@ export default {
type: Object,
required: true,
},
commitSha: {
type: String,
required: false,
default: '',
},
isNewCiConfigFile: {
type: Boolean,
required: true,
@ -54,7 +59,11 @@ export default {
</script>
<template>
<div class="gl-mb-5">
<pipeline-status v-if="showPipelineStatus" :class="$options.pipelineStatusClasses" />
<pipeline-status
v-if="showPipelineStatus"
:commit-sha="commitSha"
:class="$options.pipelineStatusClasses"
/>
<validation-segment :class="validationStyling" :ci-config="ciConfigData" />
</div>
</template>

View File

@ -3,7 +3,6 @@ import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
import getCommitSha from '~/pipeline_editor/graphql/queries/client/commit_sha.graphql';
import getPipelineQuery from '~/pipeline_editor/graphql/queries/client/pipeline.graphql';
import getPipelineEtag from '~/pipeline_editor/graphql/queries/client/pipeline_etag.graphql';
import {
@ -33,10 +32,14 @@ export default {
GlSprintf,
},
inject: ['projectFullPath'],
apollo: {
props: {
commitSha: {
query: getCommitSha,
type: String,
required: false,
default: '',
},
},
apollo: {
pipelineEtag: {
query: getPipelineEtag,
},
@ -51,7 +54,7 @@ export default {
sha: this.commitSha,
};
},
update: (data) => {
update(data) {
const { id, commitPath = '', detailedStatus = {} } = data.project?.pipeline || {};
return {
@ -60,6 +63,11 @@ export default {
detailedStatus,
};
},
result(res) {
if (res.data?.project?.pipeline) {
this.hasError = false;
}
},
error() {
this.hasError = true;
},
@ -68,7 +76,6 @@ export default {
},
data() {
return {
commitSha: '',
hasError: false,
};
},
@ -84,7 +91,11 @@ export default {
// (e.g. pipeline is null during fetch when the pipeline hasn't been
// triggered yet), we can just show the loading state until the pipeline
// details are ready to be fetched
return this.$apollo.queries.pipeline.loading || (!this.hasPipelineData && !this.hasError);
return (
this.$apollo.queries.pipeline.loading ||
this.commitSha.length === 0 ||
(!this.hasPipelineData && !this.hasError)
);
},
shortSha() {
return truncateSha(this.commitSha);

View File

@ -69,6 +69,11 @@ export default {
type: String,
required: true,
},
commitSha: {
type: String,
required: false,
default: '',
},
},
apollo: {
appStatus: {
@ -110,7 +115,7 @@ export default {
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
>
<ci-editor-header />
<text-editor :value="ciFileContent" v-on="$listeners" />
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
</editor-tab>
<editor-tab
class="gl-mb-3"

View File

@ -1,3 +0,0 @@
mutation updateCommitSha($commitSha: String) {
updateCommitSha(commitSha: $commitSha) @client
}

View File

@ -1,3 +0,0 @@
query getCommitSha {
commitSha @client
}

View File

@ -1,6 +1,5 @@
import produce from 'immer';
import axios from '~/lib/utils/axios_utils';
import getCommitShaQuery from './queries/client/commit_sha.graphql';
import getCurrentBranchQuery from './queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
@ -32,14 +31,6 @@ export const resolvers = {
__typename: 'CiLintContent',
}));
},
updateCommitSha: (_, { commitSha }, { cache }) => {
cache.writeQuery({
query: getCommitShaQuery,
data: produce(cache.readQuery({ query: getCommitShaQuery }), (draftData) => {
draftData.commitSha = commitSha;
}),
});
},
updateCurrentBranch: (_, { currentBranch }, { cache }) => {
cache.writeQuery({
query: getCurrentBranchQuery,

View File

@ -4,7 +4,6 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import { CODE_SNIPPET_SOURCE_SETTINGS } from './components/code_snippet_alert/constants';
import getCommitSha from './graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './graphql/queries/client/last_commit_branch.query.graphql';
import getPipelineEtag from './graphql/queries/client/pipeline_etag.graphql';
@ -26,7 +25,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const {
// Add to apollo cache as it can be updated by future queries
commitSha,
initialBranchName,
pipelineEtag,
// Add to provide/inject API for static values
@ -69,13 +67,6 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
},
});
cache.writeQuery({
query: getCommitSha,
data: {
commitSha,
},
});
cache.writeQuery({
query: getPipelineEtag,
data: {

View File

@ -16,11 +16,9 @@ import {
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
import updateCommitShaMutation from './graphql/mutations/update_commit_sha.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
import getCommitSha from './graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
@ -156,7 +154,32 @@ export default {
query: getAppStatus,
},
commitSha: {
query: getCommitSha,
query: getLatestCommitShaQuery,
variables() {
return {
projectPath: this.projectFullPath,
ref: this.currentBranch,
};
},
update(data) {
const pipelineNodes = data.project?.pipelines?.nodes ?? [];
// it's possible to query for the commit sha too early after an update
// (e.g. after committing a new branch, we might query for the commit sha
// but the pipeline nodes are still empty).
// in this case, we start polling until we get a commit sha.
if (pipelineNodes.length === 0) {
if (![EDITOR_APP_STATUS_LOADING, EDITOR_APP_STATUS_EMPTY].includes(this.appStatus)) {
this.$apollo.queries.commitSha.startPolling(1000);
return this.commitSha;
}
return '';
}
this.$apollo.queries.commitSha.stopPolling();
return pipelineNodes[0].sha;
},
},
currentBranch: {
query: getCurrentBranch,
@ -257,38 +280,6 @@ export default {
updateCiConfig(ciFileContent) {
this.currentCiFileContent = ciFileContent;
},
async updateCommitSha({ newBranch }) {
let fetchResults;
try {
fetchResults = await this.$apollo.query({
query: getLatestCommitShaQuery,
variables: {
projectPath: this.projectFullPath,
ref: newBranch,
},
});
} catch {
this.showFetchError();
return;
}
if (fetchResults.errors?.length > 0) {
this.showFetchError();
return;
}
const pipelineNodes = fetchResults?.data?.project?.pipelines?.nodes ?? [];
if (pipelineNodes.length === 0) {
return;
}
const commitSha = pipelineNodes[0].sha;
this.$apollo.mutate({
mutation: updateCommitShaMutation,
variables: { commitSha },
});
},
updateOnCommit({ type }) {
this.reportSuccess(type);
@ -336,12 +327,12 @@ export default {
:ci-config-data="ciConfigData"
:ci-file-content="currentCiFileContent"
:is-new-ci-config-file="isNewCiConfigFile"
:commit-sha="commitSha"
@commit="updateOnCommit"
@resetContent="resetContent"
@showError="showErrorAlert"
@refetchContent="refetchContent"
@updateCiConfig="updateCiConfig"
@updateCommitSha="updateCommitSha"
/>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>

View File

@ -25,6 +25,11 @@ export default {
type: String,
required: true,
},
commitSha: {
type: String,
required: false,
default: '',
},
isNewCiConfigFile: {
type: Boolean,
required: true,
@ -56,15 +61,22 @@ export default {
<pipeline-editor-file-nav v-on="$listeners" />
<pipeline-editor-header
:ci-config-data="ciConfigData"
:commit-sha="commitSha"
:is-new-ci-config-file="isNewCiConfigFile"
/>
<pipeline-editor-tabs
:ci-config-data="ciConfigData"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
v-on="$listeners"
@set-current-tab="setCurrentTab"
/>
<commit-section v-if="showCommitForm" :ci-file-content="ciFileContent" v-on="$listeners" />
<commit-section
v-if="showCommitForm"
:ci-file-content="ciFileContent"
:commit-sha="commitSha"
v-on="$listeners"
/>
<pipeline-editor-drawer v-if="showPipelineDrawer" />
</div>
</template>

View File

@ -2,7 +2,7 @@
td,
th,
li {
:only-child {
:first-child {
margin-bottom: 0 !important;
}
}

View File

@ -386,15 +386,6 @@
}
}
}
.boards-add-list > .btn {
text-align: left;
> svg {
position: absolute;
right: 6px;
}
}
}
.droplab-dropdown .dropdown-menu .filter-dropdown-item {

View File

@ -15,14 +15,6 @@
}
}
.dropdown-menu-issues-board-new {
width: 320px;
.dropdown-content {
max-height: 140px;
}
}
.issue-board-dropdown-content {
margin: 0;
padding: $gl-padding-4 $gl-padding $gl-padding;

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
class ApplicationRecord < ActiveRecord::Base
self.gitlab_schema = :gitlab_main
self.abstract_class = true
alias_method :reset, :reload

View File

@ -2,6 +2,7 @@
module Ci
class ApplicationRecord < ::ApplicationRecord
self.gitlab_schema = :gitlab_ci
self.abstract_class = true
def self.table_name_prefix

View File

@ -12,51 +12,55 @@ module Ci
scope :queued_before, ->(time) { where(arel_table[:created_at].lt(time)) }
scope :with_instance_runners, -> { where(instance_runners_enabled: true) }
def self.upsert_from_build!(build)
entry = self.new(args_from_build(build))
class << self
def upsert_from_build!(build)
entry = self.new(args_from_build(build))
entry.validate!
entry.validate!
self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
end
self.upsert(entry.attributes.compact, returning: %w[build_id], unique_by: :build_id)
end
def self.args_from_build(build)
args = {
build: build,
project: build.project,
protected: build.protected?,
namespace: build.project.namespace
}
private
def args_from_build(build)
args = {
build: build,
project: build.project,
protected: build.protected?,
namespace: build.project.namespace
}
if Feature.enabled?(:ci_pending_builds_maintain_tags_data, type: :development, default_enabled: :yaml)
args.store(:tag_ids, build.tags_ids)
end
if Feature.enabled?(:ci_pending_builds_maintain_shared_runners_data, type: :development, default_enabled: :yaml)
args.store(:instance_runners_enabled, shareable?(build))
end
if Feature.enabled?(:ci_pending_builds_maintain_shared_runners_data, type: :development, default_enabled: :yaml)
args.merge(instance_runners_enabled: shareable?(build))
else
args
end
end
private_class_method :args_from_build
def self.shareable?(build)
shared_runner_enabled?(build) &&
builds_access_level?(build) &&
project_not_removed?(build)
end
private_class_method :shareable?
def shareable?(build)
shared_runner_enabled?(build) &&
builds_access_level?(build) &&
project_not_removed?(build)
end
def self.shared_runner_enabled?(build)
build.project.shared_runners.exists?
end
private_class_method :shared_runner_enabled?
def shared_runner_enabled?(build)
build.project.shared_runners.exists?
end
def self.project_not_removed?(build)
!build.project.pending_delete?
end
private_class_method :project_not_removed?
def project_not_removed?(build)
!build.project.pending_delete?
end
def self.builds_access_level?(build)
build.project.project_feature.builds_access_level.nil? || build.project.project_feature.builds_access_level > 0
def builds_access_level?(build)
build.project.project_feature.builds_access_level.nil? ||
build.project.project_feature.builds_access_level > 0
end
end
private_class_method :builds_access_level?
end
end

View File

@ -3,6 +3,10 @@
module TaggableQueries
extend ActiveSupport::Concern
MAX_TAGS_IDS = 50
TooManyTagsError = Class.new(StandardError)
class_methods do
# context is a name `acts_as_taggable context`
def arel_tag_names_array(context = :tags)
@ -34,4 +38,10 @@ module TaggableQueries
where("EXISTS (?)", matcher)
end
end
def tags_ids
tags.limit(MAX_TAGS_IDS).order('id ASC').pluck(:id).tap do |ids|
raise TooManyTagsError if ids.size >= MAX_TAGS_IDS
end
end
end

View File

@ -254,8 +254,7 @@ class User < ApplicationRecord
message: _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } }
before_validation :sanitize_attrs
before_validation :set_public_email, if: :public_email_changed?
before_validation :set_commit_email, if: :commit_email_changed?
before_validation :reset_secondary_emails, if: :email_changed?
before_save :default_private_profile_to_false
before_save :set_public_email, if: :public_email_changed? # in case validation is skipped
before_save :set_commit_email, if: :commit_email_changed? # in case validation is skipped
@ -928,24 +927,6 @@ class User < ApplicationRecord
end
end
def notification_email_verified
return if read_attribute(:notification_email).blank? || temp_oauth_email?
errors.add(:notification_email, _("must be an email you have verified")) unless verified_emails.include?(notification_email)
end
def public_email_verified
return if public_email.blank?
errors.add(:public_email, _("must be an email you have verified")) unless verified_emails.include?(public_email)
end
def commit_email_verified
return if read_attribute(:commit_email).blank?
errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email)
end
# Define commit_email-related attribute methods explicitly instead of relying
# on ActiveRecord to provide them. Some of the specs use the current state of
# the model code but an older database schema, so we need to guard against the
@ -1298,24 +1279,6 @@ class User < ApplicationRecord
self.name = self.name.gsub(%r{</?[^>]*>}, '')
end
def set_notification_email
if notification_email.blank? || all_emails.exclude?(notification_email)
self.notification_email = email
end
end
def set_public_email
if public_email.blank? || all_emails.exclude?(public_email)
self.public_email = ''
end
end
def set_commit_email
if commit_email.blank? || verified_emails.exclude?(commit_email)
self.commit_email = nil
end
end
def unset_secondary_emails_matching_deleted_email!(deleted_email)
secondary_email_attribute_changed = false
SECONDARY_EMAIL_ATTRIBUTES.each do |attribute|
@ -2025,6 +1988,48 @@ class User < ApplicationRecord
private
def notification_email_verified
return if read_attribute(:notification_email).blank? || temp_oauth_email?
errors.add(:notification_email, _("must be an email you have verified")) unless verified_emails.include?(notification_email)
end
def public_email_verified
return if public_email.blank?
errors.add(:public_email, _("must be an email you have verified")) unless verified_emails.include?(public_email)
end
def commit_email_verified
return if read_attribute(:commit_email).blank?
errors.add(:commit_email, _("must be an email you have verified")) unless verified_emails.include?(commit_email)
end
def set_notification_email
if notification_email.blank? || verified_emails.exclude?(notification_email)
self.notification_email = nil
end
end
def set_public_email
if public_email.blank? || verified_emails.exclude?(public_email)
self.public_email = ''
end
end
def set_commit_email
if commit_email.blank? || verified_emails.exclude?(commit_email)
self.commit_email = nil
end
end
def reset_secondary_emails
set_public_email
set_commit_email
set_notification_email
end
def callouts_by_feature_name
@callouts_by_feature_name ||= callouts.index_by(&:feature_name)
end

View File

@ -17,11 +17,19 @@ module Ci
end
def builds_matching_tag_ids(relation, ids)
relation.merge(CommitStatus.matches_tag_ids(ids, table: 'ci_pending_builds', column: 'build_id'))
if ::Feature.enabled?(:ci_queueing_denormalize_tags_information, runner, default_enabled: :yaml)
relation.where('tag_ids <@ ARRAY[?]::int[]', runner.tags_ids)
else
relation.merge(CommitStatus.matches_tag_ids(ids, table: 'ci_pending_builds', column: 'build_id'))
end
end
def builds_with_any_tags(relation)
relation.merge(CommitStatus.with_any_tags(table: 'ci_pending_builds', column: 'build_id'))
if ::Feature.enabled?(:ci_queueing_denormalize_tags_information, runner, default_enabled: :yaml)
relation.where('cardinality(tag_ids) > 0')
else
relation.merge(CommitStatus.with_any_tags(table: 'ci_pending_builds', column: 'build_id'))
end
end
def order(relation)

View File

@ -1,8 +0,0 @@
.dropdown.gl-display-flex.gl-align-items-center.gl-ml-3#js-add-list
%button.gl-button.btn.btn-confirm.js-new-board-list{ type: "button", data: board_list_data }
Add list
.dropdown-menu.dropdown-extended-height.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" }
- if can?(current_user, :admin_label, board.resource_parent)
= render partial: "shared/issuable/label_page_create", locals: { show_add_list: true, add_list: true, add_list_class: 'd-none' }
= dropdown_loading

View File

@ -207,10 +207,7 @@
#js-board-epics-swimlanes-toggle
.js-board-config{ data: { can_admin_list: user_can_admin_list.to_s, has_scope: board.scoped?.to_s } }
- if user_can_admin_list
- if Feature.enabled?(:board_new_list, board.resource_parent, default_enabled: :yaml) || board.to_type == "EpicBoard"
.js-create-column-trigger{ data: board_list_data }
- else
= render 'shared/issuable/board_create_list_dropdown', board: board
.js-create-column-trigger{ data: board_list_data }
#js-toggle-focus-btn
- elsif type != :productivity_analytics && show_sorting_dropdown
= render 'shared/issuable/sort_dropdown'

View File

@ -1,8 +1,8 @@
---
name: between_uses_list_commits
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67591
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337960
name: ci_build_tags_limit
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68380
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338929
milestone: '14.2'
type: development
group: group::source code
group: group::pipeline execution
default_enabled: false

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/273755
milestone: '13.6'
type: development
group: group::pipeline authoring
default_enabled: false
default_enabled: true

View File

@ -1,8 +1,8 @@
---
name: board_new_list
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52061
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299366
milestone: '13.8'
name: ci_pending_builds_maintain_tags_data
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65648
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338363
milestone: '14.2'
type: development
group: group::project management
default_enabled: true
group: group::pipeline execution
default_enabled: false

View File

@ -1,8 +1,8 @@
---
name: track_unique_visits
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33146
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255994
milestone: '13.2'
name: ci_queueing_denormalize_tags_information
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65648
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338366
milestone: '14.1'
type: development
group: group::optimize
default_enabled: true
group: group::pipeline execution
default_enabled: false

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
# This parameter describes a virtual context to indicate
# table affinity to other tables.
#
# Table affinity limits cross-joins, cross-modifications,
# foreign keys and validates relationship between tables
#
# By default it is undefined
ActiveRecord::Base.class_attribute :gitlab_schema, default: nil

View File

@ -13,3 +13,7 @@ raise "Counter cache is not disabled" if
ActsAsTaggableOn::Tagging.include IgnorableColumns
ActsAsTaggableOn::Tagging.ignore_column :id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
ActsAsTaggableOn::Tagging.ignore_column :taggable_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
# The tags and taggings are supposed to be part of `gitlab_ci`
ActsAsTaggableOn::Tag.gitlab_schema = :gitlab_ci
ActsAsTaggableOn::Tagging.gitlab_schema = :gitlab_ci

View File

@ -1,20 +1,21 @@
---
data_category: optional
key_path: usage_activity_by_stage_monthly.manage.events
description:
description: Number of distinct users who have generated a manage event by month
product_section: dev
product_stage:
product_stage: manage
product_group: group::manage
product_category:
value_type: number
status: data_available
time_frame: 28d
data_source:
data_source: database
distribution:
- ce
- ee
tier:
- free
skip_validation: true
- premium
- ultimate
performance_indicator_type:
- umau

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddTagsArrayToCiPendingBuilds < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
def up
with_lock_retries do
add_column :ci_pending_builds, :tag_ids, :integer, array: true, default: []
end
end
def down
with_lock_retries do
remove_column :ci_pending_builds, :tag_ids
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddTagIdsIndexToCiPendingBuild < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
INDEX_NAME = 'index_ci_pending_builds_on_tag_ids'
def up
add_concurrent_index(:ci_pending_builds, :tag_ids, name: INDEX_NAME, where: 'cardinality(tag_ids) > 0')
end
def down
remove_concurrent_index_by_name(:ci_pending_builds, name: INDEX_NAME)
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
class PrepareIndexesForEventsBigintConversion < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
TABLE_NAME = 'events'
def up
prepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true,
name: :index_events_on_id_convert_to_bigint
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint],
name: :index_events_on_project_id_and_id_convert_to_bigint
prepare_async_index TABLE_NAME, [:project_id, :id_convert_to_bigint], order: { id_convert_to_bigint: :desc },
where: 'action = 7', name: :index_events_on_project_id_and_id_bigint_desc_on_merged_action
end
def down
unprepare_async_index_by_name TABLE_NAME, :index_events_on_id_convert_to_bigint
unprepare_async_index_by_name TABLE_NAME, :index_events_on_project_id_and_id_convert_to_bigint
unprepare_async_index_by_name TABLE_NAME, :index_events_on_project_id_and_id_bigint_desc_on_merged_action
end
end

View File

@ -0,0 +1 @@
837b9a56114c63064379cf276a3c7e2bbe845af9022a542c4fcec94a25062017

View File

@ -0,0 +1 @@
360bb1c16c93d7a6564ed70fa2dea4212e1fd00d101cfdc9017b54f67eae797d

View File

@ -0,0 +1 @@
88ca485c8513df96b1f1aec1585c385223dc53889e547db42b509b0cd1bea9b7

View File

@ -10903,7 +10903,8 @@ CREATE TABLE ci_pending_builds (
protected boolean DEFAULT false NOT NULL,
instance_runners_enabled boolean DEFAULT false NOT NULL,
namespace_id bigint,
minutes_exceeded boolean DEFAULT false NOT NULL
minutes_exceeded boolean DEFAULT false NOT NULL,
tag_ids integer[] DEFAULT '{}'::integer[]
);
CREATE SEQUENCE ci_pending_builds_id_seq
@ -23472,6 +23473,8 @@ CREATE INDEX index_ci_pending_builds_on_namespace_id ON ci_pending_builds USING
CREATE INDEX index_ci_pending_builds_on_project_id ON ci_pending_builds USING btree (project_id);
CREATE INDEX index_ci_pending_builds_on_tag_ids ON ci_pending_builds USING btree (tag_ids) WHERE (cardinality(tag_ids) > 0);
CREATE INDEX index_ci_pipeline_artifacts_failed_verification ON ci_pipeline_artifacts USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3);
CREATE INDEX index_ci_pipeline_artifacts_needs_verification ON ci_pipeline_artifacts USING btree (verification_state) WHERE ((verification_state = 0) OR (verification_state = 3));

View File

@ -1885,6 +1885,9 @@ variables:
- echo "Hello runner selector feature"
```
NOTE:
In [GitLab 14.3](https://gitlab.com/gitlab-org/gitlab/-/issues/338479) and later, the number of tags must be less than `50`.
### `allow_failure`
Use `allow_failure` when you want to let a job fail without impacting the rest of the CI

View File

@ -22,7 +22,7 @@ Be wary of [the limitations that come with using Hamlit](https://github.com/k0ku
We also use [SCSS](https://sass-lang.com) and plain JavaScript with
modern ECMAScript standards supported through [Babel](https://babeljs.io/) and ES module support through [webpack](https://webpack.js.org/).
Working with our frontend assets requires Node (v10.13.0 or greater) and Yarn
Working with our frontend assets requires Node (v12.22.1 or greater) and Yarn
(v1.10.0 or greater). You can find information on how to install these on our
[installation guide](../../install/installation.md#4-node).

View File

@ -339,14 +339,14 @@ As in other list types, click the trash icon to remove a list.
### Iteration lists **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/250479) in GitLab 13.11.
> - [Deployed behind the `board_new_list` and `iteration_board_lists` feature flags](../feature_flags.md), enabled by default.
> - Enabled on GitLab.com.
> - Recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to disable the feature flags: [`board_new_list`](#enable-or-disable-new-add-list-form) and [`iteration_board_lists`](#enable-or-disable-iteration-lists-in-boards). **(PREMIUM SELF)**
> - Enabled on GitLab.com and is ready for production use.
> - Enabled with `iteration_board_lists` flag for self-managed GitLab and is ready for production use.
> GitLab administrators can opt to [disable the feature flag](#enable-or-disable-iteration-lists-in-boards).
There can be
[risks when disabling released features](../../administration/feature_flags.md#risks-when-disabling-released-features).
Refer to this feature's version history for more details.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature, ask an
administrator to [disable the `iteration_board_lists` flag](../../administration/feature_flags.md).
On GitLab.com, this feature is available.
You're also able to create lists of an iteration.
These are lists that filter issues by the assigned
@ -675,10 +675,6 @@ A few things to remember:
### Enable or disable GraphQL-based issue boards **(FREE SELF)**
NOTE:
When enabling GraphQL-based issue boards, you must also enable the
[new add list form](#enable-or-disable-new-add-list-form).
It is deployed behind a feature flag that is **enabled by default** as of GitLab 14.1.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can disable it.
@ -695,30 +691,8 @@ To disable it:
Feature.disable(:graphql_board_lists)
```
### Enable or disable new add list form **(FREE SELF)**
The new form for adding lists 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 disable it.
To enable it:
```ruby
Feature.enable(:board_new_list)
```
To disable it:
```ruby
Feature.disable(:board_new_list)
```
### Enable or disable iteration lists in boards **(PREMIUM SELF)**
NOTE:
When disabling iteration lists in boards, you also need to disable the [new add list form](#enable-or-disable-new-add-list-form).
The iteration list 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)

View File

@ -45,7 +45,7 @@ module Gitlab
end
def read_zip_file!(file_path)
if ::Feature.enabled?(:ci_new_artifact_file_reader, job.project, default_enabled: false)
if ::Feature.enabled?(:ci_new_artifact_file_reader, job.project, default_enabled: :yaml)
read_with_new_artifact_file_reader(file_path)
else
read_with_legacy_artifact_file_reader(file_path)

View File

@ -53,7 +53,7 @@ module Gitlab
description: 'Set retry default value.',
inherit: false
entry :tags, ::Gitlab::Config::Entry::ArrayOfStrings,
entry :tags, Entry::Tags,
description: 'Set the default tags.',
inherit: false

View File

@ -85,7 +85,7 @@ module Gitlab
description: 'Retry configuration for this job.',
inherit: true
entry :tags, ::Gitlab::Config::Entry::ArrayOfStrings,
entry :tags, Entry::Tags,
description: 'Set the tags.',
inherit: true

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module Entry
##
# Entry that represents an array of tags.
#
class Tags < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
TAGS_LIMIT = 50
validations do
validates :config, array_of_strings: true
validate do
next unless ::Feature.enabled?(:ci_build_tags_limit, default_enabled: :yaml)
if config.is_a?(Array) && config.size >= TAGS_LIMIT
errors.add(:config, _("must be less than the limit of %{tag_limit} tags") % { tag_limit: TAGS_LIMIT })
end
end
end
end
end
end
end
end

View File

@ -116,13 +116,9 @@ module Gitlab
return [] if Gitlab::Git.blank_ref?(base) || Gitlab::Git.blank_ref?(head)
wrapped_gitaly_errors do
if Feature.enabled?(:between_uses_list_commits, default_enabled: :yaml)
revisions = [head, "^#{base}"] # base..head
revisions = [head, "^#{base}"] # base..head
repo.gitaly_commit_client.list_commits(revisions, reverse: true)
else
repo.gitaly_commit_client.between(base, head)
end
repo.gitaly_commit_client.list_commits(revisions, reverse: true)
end
end

View File

@ -2,84 +2,67 @@
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: i_analytics_dev_ops_adoption
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: i_analytics_dev_ops_score
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: p_analytics_merge_request
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: i_analytics_instance_statistics
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: g_analytics_contribution
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: g_analytics_insights
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: g_analytics_issues
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: g_analytics_productivity
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: g_analytics_valuestream
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: p_analytics_pipelines
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: p_analytics_code_reviews
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: p_analytics_valuestream
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: p_analytics_insights
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: p_analytics_issues
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: p_analytics_repo
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits
- name: i_analytics_cohorts
category: analytics
redis_slot: analytics
aggregation: weekly
feature_flag: track_unique_visits

View File

@ -4,27 +4,22 @@
redis_slot: compliance
category: compliance
aggregation: weekly
feature_flag: track_unique_visits
- name: g_compliance_audit_events
category: compliance
redis_slot: compliance
aggregation: weekly
feature_flag: track_unique_visits
- name: i_compliance_audit_events
category: compliance
redis_slot: compliance
aggregation: weekly
feature_flag: track_unique_visits
- name: i_compliance_credential_inventory
category: compliance
redis_slot: compliance
aggregation: weekly
feature_flag: track_unique_visits
- name: a_compliance_audit_events_api
category: compliance
redis_slot: compliance
aggregation: weekly
feature_flag: track_unique_visits
- name: g_edit_by_web_ide
category: ide_edit
redis_slot: edit

View File

@ -4650,9 +4650,6 @@ msgstr ""
msgid "Assignee lists not available with your current license"
msgstr ""
msgid "Assignee lists show all issues assigned to the selected user."
msgstr ""
msgid "Assignee(s)"
msgstr ""
@ -19448,9 +19445,6 @@ msgstr ""
msgid "Label actions dropdown"
msgstr ""
msgid "Label lists show all issues with the selected label."
msgstr ""
msgid "Label priority"
msgstr ""
@ -21526,9 +21520,6 @@ msgstr ""
msgid "Milestone lists not available with your current license"
msgstr ""
msgid "Milestone lists show all issues from the selected milestone."
msgstr ""
msgid "MilestoneCombobox|An error occurred while searching for milestones"
msgstr ""
@ -39962,6 +39953,9 @@ msgstr ""
msgid "must be inside the fork network"
msgstr ""
msgid "must be less than the limit of %{tag_limit} tags"
msgstr ""
msgid "must be unique by status and elapsed time within a policy"
msgstr ""

View File

@ -277,7 +277,7 @@
"chokidar": "^3.4.0"
},
"engines": {
"node": ">=10.13.0",
"node": ">=12.22.1",
"yarn": "^1.10.0"
}
}

View File

@ -11,10 +11,6 @@ FactoryBot.define do
confirmation_token { nil }
can_create_group { true }
after(:stub) do |user|
user.notification_email = user.email
end
trait :admin do
admin { true }
end

View File

@ -4,8 +4,20 @@ require 'spec_helper'
RSpec.describe "Dashboard Issues Feed" do
describe "GET /issues" do
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:user) do
user = create(:user, email: 'private1@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
user.update!(public_email: public_email.email)
user
end
let!(:assignee) do
user = create(:user, email: 'private2@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
user.update!(public_email: public_email.email)
user
end
let!(:project1) { create(:project) }
let!(:project2) { create(:project) }

View File

@ -4,11 +4,23 @@ require 'spec_helper'
RSpec.describe 'Issues Feed' do
describe 'GET /issues' do
let_it_be_with_reload(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let_it_be(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, author: user, assignees: [assignee], project: project, due_date: Date.today) }
let_it_be_with_reload(:user) do
user = create(:user, email: 'private1@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
user.update!(public_email: public_email.email)
user
end
let_it_be(:assignee) do
user = create(:user, email: 'private2@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
user.update!(public_email: public_email.email)
user
end
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, author: user, assignees: [assignee], project: project, due_date: Date.today) }
let_it_be(:issuable) { issue } # "alias" for shared examples
before_all do

View File

@ -17,6 +17,8 @@ RSpec.describe 'User adds lists', :js do
let_it_be(:project_label) { create(:label, project: project) }
let_it_be(:group_backlog_list) { create(:backlog_list, board: group_board) }
let_it_be(:project_backlog_list) { create(:backlog_list, board: project_board) }
let_it_be(:backlog) { create(:group_label, group: group, name: 'Backlog') }
let_it_be(:closed) { create(:group_label, group: group, name: 'Closed') }
let_it_be(:issue) { create(:labeled_issue, project: project, labels: [group_label, project_label]) }
@ -25,15 +27,11 @@ RSpec.describe 'User adds lists', :js do
group.add_owner(user)
end
where(:board_type, :graphql_board_lists_enabled, :board_new_list_enabled) do
:project | true | true
:project | false | true
:project | true | false
:project | false | false
:group | true | true
:group | false | true
:group | true | false
:group | false | false
where(:board_type, :graphql_board_lists_enabled) do
:project | true
:project | false
:group | true
:group | false
end
with_them do
@ -43,8 +41,7 @@ RSpec.describe 'User adds lists', :js do
set_cookie('sidebar_collapsed', 'true')
stub_feature_flags(
graphql_board_lists: graphql_board_lists_enabled,
board_new_list: board_new_list_enabled
graphql_board_lists: graphql_board_lists_enabled
)
if board_type == :project
@ -57,39 +54,44 @@ RSpec.describe 'User adds lists', :js do
end
it 'creates new column for label containing labeled issue' do
click_button button_text(board_new_list_enabled)
click_button 'Create list'
wait_for_all_requests
select_label(board_new_list_enabled, group_label)
select_label(group_label)
wait_for_all_requests
expect(page).to have_selector('.board', text: group_label.title)
expect(find('.board:nth-child(2) .board-card')).to have_content(issue.title)
end
end
def select_label(board_new_list_enabled, label)
if board_new_list_enabled
click_button 'Select a label'
it 'creates new list for Backlog and closed labels' do
click_button 'Create list'
wait_for_requests
find('label', text: label.title).click
select_label(backlog)
click_button 'Add to board'
click_button 'Create list'
wait_for_requests
wait_for_all_requests
else
page.within('.dropdown-menu-issues-board-new') do
click_link label.title
end
select_label(closed)
wait_for_requests
expect(page).to have_selector('.board', text: closed.title)
expect(find('.board:nth-child(2) .board-header')).to have_content(backlog.title)
expect(find('.board:nth-child(3) .board-header')).to have_content(closed.title)
expect(find('.board:nth-child(4) .board-header')).to have_content('Closed')
end
end
def button_text(board_new_list_enabled)
if board_new_list_enabled
'Create list'
else
'Add list'
end
def select_label(label)
click_button 'Select a label'
find('label', text: label.title).click
click_button 'Add to board'
wait_for_all_requests
end
end

View File

@ -4,8 +4,20 @@ require 'spec_helper'
RSpec.describe 'Dashboard Issues Calendar Feed' do
describe 'GET /issues' do
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:user) do
user = create(:user, email: 'private1@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
user.update!(public_email: public_email.email)
user
end
let!(:assignee) do
user = create(:user, email: 'private2@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
user.update!(public_email: public_email.email)
user
end
let!(:project) { create(:project) }
let(:milestone) { create(:milestone, project_id: project.id, title: 'v1.0') }

View File

@ -4,8 +4,20 @@ require 'spec_helper'
RSpec.describe 'Group Issues Calendar Feed' do
describe 'GET /issues' do
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:user) do
user = create(:user, email: 'private1@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
user.update!(public_email: public_email.email)
user
end
let!(:assignee) do
user = create(:user, email: 'private2@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
user.update!(public_email: public_email.email)
user
end
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }

View File

@ -4,8 +4,20 @@ require 'spec_helper'
RSpec.describe 'Project Issues Calendar Feed' do
describe 'GET /issues' do
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:user) do
user = create(:user, email: 'private1@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public1@example.com')
user.update!(public_email: public_email.email)
user
end
let!(:assignee) do
user = create(:user, email: 'private2@example.com')
public_email = create(:email, :confirmed, user: user, email: 'public2@example.com')
user.update!(public_email: public_email.email)
user
end
let!(:project) { create(:project) }
let!(:issue) { create(:issue, author: user, assignees: [assignee], project: project) }

View File

@ -34,7 +34,7 @@ RSpec.describe "User views issues" do
.and have_content(open_issue2.title)
.and have_no_content(closed_issue.title)
.and have_content(moved_open_issue.title)
.and have_no_selector(".js-new-board-list")
.and have_no_content('Create list')
end
it "opens issues by label" do
@ -65,7 +65,7 @@ RSpec.describe "User views issues" do
.and have_no_content(open_issue1.title)
.and have_no_content(open_issue2.title)
.and have_no_content(moved_open_issue.title)
.and have_no_selector(".js-new-board-list")
.and have_no_content('Create list')
end
include_examples "opens issue from list" do
@ -87,7 +87,7 @@ RSpec.describe "User views issues" do
.and have_content(open_issue2.title)
.and have_content(moved_open_issue.title)
.and have_no_content('CLOSED (MOVED)')
.and have_no_selector(".js-new-board-list")
.and have_no_content('Create list')
end
include_examples "opens issue from list" do

View File

@ -17,7 +17,6 @@ RSpec.describe 'Labels Hierarchy', :js do
let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') }
before do
stub_feature_flags(board_new_list: false)
grandparent.add_owner(user)
sign_in(user)
@ -307,88 +306,4 @@ RSpec.describe 'Labels Hierarchy', :js do
it_behaves_like 'filtering by ancestor labels for groups', true
end
end
context 'creating boards lists' do
before do
stub_feature_flags(board_new_list: false)
end
context 'on project boards' do
let(:board) { create(:board, project: project_1) }
before do
project_1.add_developer(user)
visit project_board_path(project_1, board)
find('.js-new-board-list').click
wait_for_requests
end
it 'creates lists from all ancestor labels' do
[grandparent_group_label, parent_group_label, project_label_1].each do |label|
find('a', text: label.title).click
end
wait_for_requests
expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
expect(page).to have_selector('.board-title-text', text: project_label_1.title)
end
end
context 'on group boards' do
let(:board) { create(:board, group: parent) }
before do
parent.add_developer(user)
visit group_board_path(parent, board)
find('.js-new-board-list').click
wait_for_requests
end
context 'when graphql_board_lists FF enabled' do
it 'creates lists from all ancestor group labels' do
[grandparent_group_label, parent_group_label].each do |label|
find('a', text: label.title).click
end
wait_for_requests
expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
end
it 'does not create lists from descendant groups' do
expect(page).not_to have_selector('a', text: child_group_label.title)
end
end
end
context 'when graphql_board_lists FF disabled' do
let(:board) { create(:board, group: parent) }
before do
stub_feature_flags(graphql_board_lists: false)
parent.add_developer(user)
visit group_board_path(parent, board)
find('.js-new-board-list').click
wait_for_requests
end
it 'creates lists from all ancestor group labels' do
[grandparent_group_label, parent_group_label].each do |label|
find('a', text: label.title).click
end
wait_for_requests
expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
end
it 'does not create lists from descendant groups' do
expect(page).not_to have_selector('a', text: child_group_label.title)
end
end
end
end

View File

@ -1,5 +1,6 @@
import { GlFormTextarea, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
@ -48,7 +49,10 @@ describe('Pipeline Editor | Commit section', () => {
let wrapper;
let mockMutate;
const defaultProps = { ciFileContent: mockCiYml };
const defaultProps = {
ciFileContent: mockCiYml,
commitSha: mockCommitSha,
};
const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => {
mockMutate = jest.fn().mockResolvedValue({
@ -67,7 +71,6 @@ describe('Pipeline Editor | Commit section', () => {
provide: { ...mockProvide, ...provide },
data() {
return {
commitSha: mockCommitSha,
currentBranch: mockDefaultBranch,
isNewCiConfigFile: Boolean(options?.isNewCiConfigfile),
};
@ -97,8 +100,7 @@ describe('Pipeline Editor | Commit section', () => {
await findCommitForm().find('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest);
}
await findCommitForm().find('[type="submit"]').trigger('click');
// Simulate the write to local cache that occurs after a commit
await wrapper.setData({ commitSha: mockCommitNextSha });
await waitForPromises();
};
const cancelCommitForm = async () => {
@ -188,7 +190,6 @@ describe('Pipeline Editor | Commit section', () => {
update: expect.any(Function),
variables: {
...mockVariables,
lastCommitId: mockCommitNextSha,
branch: mockDefaultBranch,
},
});

View File

@ -42,15 +42,12 @@ describe('Pipeline Editor | Text editor component', () => {
defaultBranch: mockDefaultBranch,
glFeatures,
},
propsData: {
commitSha: mockCommitSha,
},
attrs: {
value: mockCiYml,
},
// Simulate graphQL client query result
data() {
return {
commitSha: mockCommitSha,
};
},
listeners: {
[EDITOR_READY_EVENT]: editorReadyListener,
},

View File

@ -247,15 +247,6 @@ describe('Pipeline editor branch switcher', () => {
expect(wrapper.emitted('refetchContent')).toBeUndefined();
});
it('emits the updateCommitSha event when selecting a different branch', async () => {
expect(wrapper.emitted('updateCommitSha')).toBeUndefined();
const branch = findDropdownItems().at(1);
branch.vm.$emit('click');
expect(wrapper.emitted('updateCommitSha')).toHaveLength(1);
});
});
describe('when searching', () => {

View File

@ -27,13 +27,11 @@ describe('Pipeline Status', () => {
wrapper = shallowMount(PipelineStatus, {
localVue,
apolloProvider: mockApollo,
propsData: {
commitSha: mockCommitSha,
},
provide: mockProvide,
stubs: { GlLink, GlSprintf },
data() {
return {
commitSha: mockCommitSha,
};
},
});
};

View File

@ -156,35 +156,33 @@ export const mergeUnwrappedCiConfig = (mergedConfig) => {
};
};
export const mockNewCommitShaResults = {
export const mockCommitShaResults = {
data: {
project: {
pipelines: {
nodes: [
{
id: 'gid://gitlab/Ci::Pipeline/1',
sha: 'd0d56d363d8a3f67a8ab9fc00207d468f30032ca',
sha: mockCommitSha,
path: `/${mockProjectFullPath}/-/pipelines/488`,
commitPath: `/${mockProjectFullPath}/-/commit/d0d56d363d8a3f67a8ab9fc00207d468f30032ca`,
},
{
id: 'gid://gitlab/Ci::Pipeline/2',
sha: 'fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa',
path: `/${mockProjectFullPath}/-/pipelines/487`,
commitPath: `/${mockProjectFullPath}/-/commit/fcab2ece40b26f428dfa3aa288b12c3c5bdb06aa`,
},
{
id: 'gid://gitlab/Ci::Pipeline/3',
sha: '6c16b17c7f94a438ae19a96c285bb49e3c632cf4',
path: `/${mockProjectFullPath}/-/pipelines/433`,
commitPath: `/${mockProjectFullPath}/-/commit/6c16b17c7f94a438ae19a96c285bb49e3c632cf4`,
},
],
},
},
},
};
export const mockEmptyCommitShaResults = {
data: {
project: {
pipelines: {
nodes: [],
},
},
},
};
export const mockProjectBranches = {
data: {
project: {

View File

@ -26,9 +26,10 @@ import {
mockBlobContentQueryResponseNoCiFile,
mockCiYml,
mockCommitSha,
mockCommitShaResults,
mockDefaultBranch,
mockEmptyCommitShaResults,
mockProjectFullPath,
mockNewCommitShaResults,
} from './mock_data';
const localVue = createLocalVue();
@ -54,7 +55,6 @@ describe('Pipeline editor app component', () => {
let mockBlobContentData;
let mockCiConfigData;
let mockGetTemplate;
let mockUpdateCommitSha;
let mockLatestCommitShaQuery;
let mockPipelineQuery;
@ -71,6 +71,11 @@ describe('Pipeline editor app component', () => {
SourceEditor: MockSourceEditor,
PipelineEditorEmptyState,
},
data() {
return {
commitSha: '',
};
},
mocks: {
$apollo: {
queries: {
@ -96,18 +101,7 @@ describe('Pipeline editor app component', () => {
[getPipelineQuery, mockPipelineQuery],
];
const resolvers = {
Query: {
commitSha() {
return mockCommitSha;
},
},
Mutation: {
updateCommitSha: mockUpdateCommitSha,
},
};
mockApollo = createMockApollo(handlers, resolvers);
mockApollo = createMockApollo(handlers);
const options = {
localVue,
@ -137,7 +131,6 @@ describe('Pipeline editor app component', () => {
mockBlobContentData = jest.fn();
mockCiConfigData = jest.fn();
mockGetTemplate = jest.fn();
mockUpdateCommitSha = jest.fn();
mockLatestCommitShaQuery = jest.fn();
mockPipelineQuery = jest.fn();
});
@ -159,11 +152,16 @@ describe('Pipeline editor app component', () => {
beforeEach(() => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
mockCiConfigData.mockResolvedValue(mockCiConfigQueryResponse);
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
});
describe('when file exists', () => {
beforeEach(async () => {
await createComponentWithApollo();
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
.mockImplementation(jest.fn());
});
it('shows pipeline editor home component', () => {
@ -181,18 +179,32 @@ describe('Pipeline editor app component', () => {
sha: mockCommitSha,
});
});
it('does not poll for the commit sha', () => {
expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
});
});
describe('when no CI config file exists', () => {
it('shows an empty state and does not show editor home component', async () => {
beforeEach(async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
await createComponentWithApollo();
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
.mockImplementation(jest.fn());
});
it('shows an empty state and does not show editor home component', async () => {
expect(findEmptyState().exists()).toBe(true);
expect(findAlert().exists()).toBe(false);
expect(findEditorHome().exists()).toBe(false);
});
it('does not poll for the commit sha', () => {
expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0);
});
describe('because of a fetching error', () => {
it('shows a unkown error message', async () => {
const loadUnknownFailureText = 'The CI configuration was not loaded, please try again.';
@ -230,6 +242,7 @@ describe('Pipeline editor app component', () => {
describe('when landing on the empty state with feature flag on', () => {
it('user can click on CTA button and see an empty editor', async () => {
mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponseNoCiFile);
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
await createComponentWithApollo({
provide: {
@ -254,9 +267,9 @@ describe('Pipeline editor app component', () => {
const updateSuccessMessage = 'Your changes have been successfully committed.';
describe('and the commit mutation succeeds', () => {
beforeEach(() => {
beforeEach(async () => {
window.scrollTo = jest.fn();
createComponent();
await createComponentWithApollo();
findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
});
@ -268,7 +281,32 @@ describe('Pipeline editor app component', () => {
it('scrolls to the top of the page to bring attention to the confirmation message', () => {
expect(window.scrollTo).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
});
it('polls for commit sha while pipeline data is not yet available', async () => {
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling')
.mockImplementation(jest.fn());
// simulate updating current branch (which triggers commitSha refetch)
// while pipeline data is not yet available
mockLatestCommitShaQuery.mockResolvedValue(mockEmptyCommitShaResults);
await wrapper.vm.$apollo.queries.commitSha.refetch();
expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(1);
});
it('stops polling for commit sha when pipeline data is available', async () => {
jest
.spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling')
.mockImplementation(jest.fn());
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
await wrapper.vm.$apollo.queries.commitSha.refetch();
expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1);
});
});
describe('and the commit mutation fails', () => {
const commitFailedReasons = ['Commit failed'];
@ -320,6 +358,10 @@ describe('Pipeline editor app component', () => {
});
describe('when refetching content', () => {
beforeEach(() => {
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
});
it('refetches blob content', async () => {
await createComponentWithApollo();
jest
@ -352,6 +394,7 @@ describe('Pipeline editor app component', () => {
const originalLocation = window.location.href;
beforeEach(() => {
mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults);
setWindowLocation('?template=Android');
});
@ -371,45 +414,4 @@ describe('Pipeline editor app component', () => {
expect(findTextEditor().exists()).toBe(true);
});
});
describe('when updating commit sha', () => {
const newCommitSha = mockNewCommitShaResults.data.project.pipelines.nodes[0].sha;
beforeEach(async () => {
mockUpdateCommitSha.mockResolvedValue(newCommitSha);
mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults);
await createComponentWithApollo();
});
it('fetches updated commit sha for the new branch', async () => {
expect(mockLatestCommitShaQuery).not.toHaveBeenCalled();
wrapper
.findComponent(PipelineEditorHome)
.vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
await waitForPromises();
expect(mockLatestCommitShaQuery).toHaveBeenCalledWith({
projectPath: mockProjectFullPath,
ref: 'new-branch',
});
});
it('updates commit sha with the newly fetched commit sha', async () => {
expect(mockUpdateCommitSha).not.toHaveBeenCalled();
wrapper
.findComponent(PipelineEditorHome)
.vm.$emit('updateCommitSha', { newBranch: 'new-branch' });
await waitForPromises();
expect(mockUpdateCommitSha).toHaveBeenCalled();
expect(mockUpdateCommitSha).toHaveBeenCalledWith(
expect.any(Object),
{ commitSha: mockNewCommitShaResults.data.project.pipelines.nodes[0].sha },
expect.any(Object),
expect.any(Object),
);
});
});
});

View File

@ -498,7 +498,7 @@ RSpec.describe ProjectsHelper do
context 'user has a configured commit email' do
before do
confirmed_email = create(:email, :confirmed, user: user)
user.update!(commit_email: confirmed_email)
user.update!(commit_email: confirmed_email.email)
end
it 'returns the commit email' do

View File

@ -618,6 +618,29 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do
end
end
end
context 'when job is using tags' do
context 'when limit is reached' do
let(:tags) { Array.new(100) { |i| "tag-#{i}" } }
let(:config) { { tags: tags, script: 'test' } }
it 'returns error', :aggregate_failures do
expect(entry).not_to be_valid
expect(entry.errors)
.to include "tags config must be less than the limit of #{Gitlab::Ci::Config::Entry::Tags::TAGS_LIMIT} tags"
end
end
context 'when limit is not reached' do
let(:config) { { tags: %w[tag1 tag2], script: 'test' } }
it 'returns a valid entry', :aggregate_failures do
expect(entry).to be_valid
expect(entry.errors).to be_empty
expect(entry.tags).to eq(%w[tag1 tag2])
end
end
end
end
describe '#manual_action?' do

View File

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Entry::Tags do
let(:entry) { described_class.new(config) }
describe 'validation' do
context 'when tags config value is correct' do
let(:config) { %w[tag1 tag2] }
describe '#value' do
it 'returns tags configuration' do
expect(entry.value).to eq config
end
end
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when entry value is not correct' do
describe '#errors' do
context 'when tags config is not an array of strings' do
let(:config) { [1, 2] }
it 'reports error' do
expect(entry.errors)
.to include 'tags config should be an array of strings'
end
end
context 'when tags limit is reached' do
let(:config) { Array.new(50) {|i| "tag-#{i}" } }
context 'when ci_build_tags_limit is enabled' do
before do
stub_feature_flags(ci_build_tags_limit: true)
end
it 'reports error' do
expect(entry.errors)
.to include "tags config must be less than the limit of #{described_class::TAGS_LIMIT} tags"
end
end
context 'when ci_build_tags_limit is disabled' do
before do
stub_feature_flags(ci_build_tags_limit: false)
end
it 'does not report an error' do
expect(entry.errors).to be_empty
end
end
end
end
end
end
end

View File

@ -370,14 +370,6 @@ RSpec.describe Gitlab::Git::Commit, :seed_helper do
end
it { is_expected.to contain_exactly(SeedRepo::Commit::ID) }
context 'between_uses_list_commits FF disabled' do
before do
stub_feature_flags(between_uses_list_commits: false)
end
it { is_expected.to contain_exactly(SeedRepo::Commit::ID) }
end
end
describe '.shas_with_signatures' do

View File

@ -113,5 +113,31 @@ RSpec.describe Ci::PendingBuild do
end
end
end
context 'when build has tags' do
let!(:build) { create(:ci_build, :tags) }
subject(:ci_pending_build) { described_class.last }
context 'when ci_pending_builds_maintain_tags_data is enabled' do
it 'sets tag_ids' do
described_class.upsert_from_build!(build)
expect(ci_pending_build.tag_ids).to eq(build.tags_ids)
end
end
context 'when ci_pending_builds_maintain_tags_data is disabled' do
before do
stub_feature_flags(ci_pending_builds_maintain_tags_data: false)
end
it 'does not set tag_ids' do
described_class.upsert_from_build!(build)
expect(ci_pending_build.tag_ids).to be_empty
end
end
end
end
end

View File

@ -465,24 +465,19 @@ RSpec.describe User do
user.commit_email = confirmed.email
expect(user).to be_valid
expect(user.commit_email).to eq(confirmed.email)
end
it 'can not be set to an unconfirmed email' do
unconfirmed = create(:email, user: user)
user.commit_email = unconfirmed.email
# This should set the commit_email attribute to the primary email
expect(user).to be_valid
expect(user.commit_email).to eq(user.email)
expect(user).not_to be_valid
end
it 'can not be set to a non-existent email' do
user.commit_email = 'non-existent-email@nonexistent.nonexistent'
# This should set the commit_email attribute to the primary email
expect(user).to be_valid
expect(user.commit_email).to eq(user.email)
expect(user).not_to be_valid
end
it 'can not be set to an invalid email, even if confirmed' do
@ -691,75 +686,6 @@ RSpec.describe User do
end
end
end
context 'owns_notification_email' do
it 'accepts temp_oauth_email emails' do
user = build(:user, email: "temp-email-for-oauth@example.com")
expect(user).to be_valid
end
it 'does not accept not verified emails' do
email = create(:email)
user = email.user
user.notification_email = email.email
expect(user).to be_invalid
expect(user.errors[:notification_email]).to include(_('must be an email you have verified'))
end
end
context 'owns_public_email' do
it 'accepts verified emails' do
email = create(:email, :confirmed, email: 'test@test.com')
user = email.user
user.notification_email = email.email
expect(user).to be_valid
end
it 'does not accept not verified emails' do
email = create(:email)
user = email.user
user.public_email = email.email
expect(user).to be_invalid
expect(user.errors[:public_email]).to include(_('must be an email you have verified'))
end
end
context 'set_commit_email' do
it 'keeps commit email when private commit email is being used' do
user = create(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
expect(user.read_attribute(:commit_email)).to eq(Gitlab::PrivateCommitEmail::TOKEN)
end
it 'keeps the commit email when nil' do
user = create(:user, commit_email: nil)
expect(user.read_attribute(:commit_email)).to be_nil
end
it 'reverts to nil when email is not verified' do
user = create(:user, commit_email: "foo@bar.com")
expect(user.read_attribute(:commit_email)).to be_nil
end
end
context 'owns_commit_email' do
it 'accepts private commit email' do
user = build(:user, commit_email: Gitlab::PrivateCommitEmail::TOKEN)
expect(user).to be_valid
end
it 'accepts nil commit email' do
user = build(:user, commit_email: nil)
expect(user).to be_valid
end
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
describe 'tags:' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.owner }
let(:ref) { 'refs/heads/master' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
let(:pipeline) { service.execute(source).payload }
before do
stub_ci_pipeline_yaml_file(config)
end
context 'with valid config' do
let(:config) { YAML.dump({ test: { script: 'ls', tags: %w[tag1 tag2] } }) }
it 'creates a pipeline', :aggregate_failures do
expect(pipeline).to be_created_successfully
expect(pipeline.builds.first.tag_list).to eq(%w[tag1 tag2])
end
end
context 'with too many tags' do
let(:tags) { Array.new(50) {|i| "tag-#{i}" } }
let(:config) { YAML.dump({ test: { script: 'ls', tags: tags } }) }
it 'creates a pipeline without builds', :aggregate_failures do
expect(pipeline).not_to be_created_successfully
expect(pipeline.builds).to be_empty
expect(pipeline.yaml_errors).to eq("jobs:test:tags config must be less than the limit of #{Gitlab::Ci::Config::Entry::Tags::TAGS_LIMIT} tags")
end
end
end
end

View File

@ -739,6 +739,22 @@ module Ci
include_examples 'handles runner assignment'
end
context 'with ci_queueing_denormalize_tags_information enabled' do
before do
stub_feature_flags(ci_queueing_denormalize_tags_information: true)
end
include_examples 'handles runner assignment'
end
context 'with ci_queueing_denormalize_tags_information disabled' do
before do
stub_feature_flags(ci_queueing_denormalize_tags_information: false)
end
include_examples 'handles runner assignment'
end
end
context 'when not using pending builds table' do

View File

@ -1,22 +0,0 @@
# frozen_string_literal: true
# This module stores the CI-related database tables which are
# going to be moved to a separate database.
module Database
module CiTables
def self.include?(name)
ci_tables.include?(name)
end
def self.ci_tables
@@ci_tables ||= Set.new.tap do |tables| # rubocop:disable Style/ClassVars
tables.merge(Ci::ApplicationRecord.descendants.map(&:table_name).compact)
# It was decided that taggings/tags are best placed with CI
# https://gitlab.com/gitlab-org/gitlab/-/issues/333413
tables.add('taggings')
tables.add('tags')
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
# This module gathes information about table to schema mapping
# to understand table affinity
module Database
module GitlabSchema
def self.table_schemas(tables)
tables.map { |table| table_schema(table) }.to_set
end
def self.table_schema(name)
tables_to_schema[name] || :undefined
end
def self.tables_to_schema
@tables_to_schema ||= all_classes_with_schema.to_h do |klass|
[klass.table_name, klass.gitlab_schema]
end
end
def self.all_classes_with_schema
ActiveRecord::Base.descendants.reject(&:abstract_class?).select(&:gitlab_schema?) # rubocop:disable Database/MultipleDatabases
end
end
end

View File

@ -75,17 +75,17 @@ module Database
return if cross_database_context[:transaction_depth_by_db].values.all?(&:zero?)
tables = PgQuery.parse(sql).dml_tables
return if tables.empty?
cross_database_context[:modified_tables_by_db][database].merge(tables)
all_tables = cross_database_context[:modified_tables_by_db].values.map(&:to_a).flatten
schemas = Database::GitlabSchema.table_schemas(all_tables)
unless PreventCrossJoins.only_ci_or_only_main?(all_tables)
if schemas.many?
raise Database::PreventCrossDatabaseModification::CrossDatabaseModificationAcrossUnsupportedTablesError,
"Cross-database data modification queries (CI and Main) were detected within " \
"a transaction '#{all_tables.join(", ")}' discovered"
"Cross-database data modification of '#{schemas.to_a.join(", ")}' were detected within " \
"a transaction modifying the '#{all_tables.to_a.join(", ")}'"
end
end
end

View File

@ -27,20 +27,15 @@ module Database
# PgQuery might fail in some cases due to limited nesting:
# https://github.com/pganalyze/pg_query/issues/209
tables = PgQuery.parse(sql).tables
schemas = Database::GitlabSchema.table_schemas(tables)
unless only_ci_or_only_main?(tables)
if schemas.many?
raise CrossJoinAcrossUnsupportedTablesError,
"Unsupported cross-join across '#{tables.join(", ")}' discovered " \
"Unsupported cross-join across '#{tables.join(", ")}' modifying '#{schemas.to_a.join(", ")}' discovered " \
"when executing query '#{sql}'"
end
end
# Returns true if a set includes only CI tables, or includes only non-CI tables
def self.only_ci_or_only_main?(tables)
tables.all? { |table| CiTables.include?(table) } ||
tables.none? { |table| CiTables.include?(table) }
end
module SpecHelpers
def with_cross_joins_prevented
subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|

View File

@ -3,14 +3,10 @@
RSpec.shared_examples 'multiple issue boards' do
context 'authorized user' do
before do
stub_feature_flags(board_new_list: false)
parent.add_maintainer(user)
login_as(user)
stub_feature_flags(board_new_list: false)
visit boards_path
wait_for_requests
end
@ -79,13 +75,13 @@ RSpec.shared_examples 'multiple issue boards' do
expect(page).to have_content(board2.name)
end
click_button 'Add list'
click_button 'Create list'
wait_for_requests
click_button 'Select a label'
page.within '.dropdown-menu-issues-board-new' do
click_link planning.title
end
page.choose(planning.title)
click_button 'Add to board'
wait_for_requests

View File

@ -66,7 +66,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
pipeline.touch
end
end
end.to raise_error /Cross-database data modification queries/
end.to raise_error /Cross-database data modification/
end
end
@ -84,7 +84,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
context 'when data modification happens in a transaction' do
it 'raises error' do
Project.transaction do
expect { run_queries }.to raise_error /Cross-database data modification queries/
expect { run_queries }.to raise_error /Cross-database data modification/
end
end
@ -93,7 +93,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
Project.transaction(requires_new: true) do
project.touch
Project.transaction(requires_new: true) do
expect { pipeline.touch }.to raise_error /Cross-database data modification queries/
expect { pipeline.touch }.to raise_error /Cross-database data modification/
end
end
end
@ -127,7 +127,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
ApplicationRecord.transaction do
create(:ci_pipeline)
end
end.to raise_error /Cross-database data modification queries/
end.to raise_error /Cross-database data modification/
end
it 'skips raising error on factory creation' do