Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-10 18:12:35 +00:00
parent 26881dd926
commit 6fd750c192
138 changed files with 1514 additions and 889 deletions

View File

@ -105,5 +105,16 @@ In this rollout issue, ensure the scoped `experiment::` label is kept accurate.
/chatops run feature set <experiment-key> false /chatops run feature set <experiment-key> false
``` ```
## Experiment Successful Cleanup Concerns
_Items to be considered if candidate experience is to become a permanent part of GitLab_
<!--
Add a list of items raised during MR review or otherwise that may need further thought/condideration
before making it a permanent part of the product.
Example: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70451#note_727246104
-->
/label ~"feature flag" ~"devops::growth" ~"growth experiment" ~"experiment-rollout" ~Engineering ~"workflow::scheduling" ~"experiment::pending" /label ~"feature flag" ~"devops::growth" ~"growth experiment" ~"experiment-rollout" ~Engineering ~"workflow::scheduling" ~"experiment::pending"
/milestone %"Next 1-3 releases" /milestone %"Next 1-3 releases"

View File

@ -14,7 +14,6 @@ import {
import { escape } from 'lodash'; import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller'; import { IdState } from 'vendor/vue-virtual-scroller';
import { diffViewerModes } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils'; import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import { __, s__, sprintf } from '~/locale'; import { __, s__, sprintf } from '~/locale';
@ -181,7 +180,7 @@ export default {
return this.diffFile.renamed_file; return this.diffFile.renamed_file;
}, },
isModeChanged() { isModeChanged() {
return this.diffFile.viewer.name === diffViewerModes.mode_changed; return this.diffFile.mode_changed;
}, },
expandDiffToFullFileTitle() { expandDiffToFullFileTitle() {
if (this.diffFile.isShowingFullFile) { if (this.diffFile.isShowingFullFile) {

View File

@ -3,7 +3,12 @@ import { get } from 'lodash';
import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants'; import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants';
function getExperimentsData() { function getExperimentsData() {
return get(window, ['gon', 'experiment'], {}); // Pull from deprecated window.gon.experiment
const experimentsFromGon = get(window, ['gon', 'experiment'], {});
// Pull from preferred window.gl.experiments
const experimentsFromGl = get(window, ['gl', 'experiments'], {});
return { ...experimentsFromGon, ...experimentsFromGl };
} }
function convertExperimentDataToExperimentContext(experimentData) { function convertExperimentDataToExperimentContext(experimentData) {

View File

@ -1,5 +1,6 @@
<script> <script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { GlAlert, GlLink, GlSprintf, GlEmptyState } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapState, mapMutations } from 'vuex'; import { mapState, mapMutations } from 'vuex';
import { retrieveAlert } from '~/jira_connect/subscriptions/utils'; import { retrieveAlert } from '~/jira_connect/subscriptions/utils';
import { SET_ALERT } from '../store/mutation_types'; import { SET_ALERT } from '../store/mutation_types';
@ -13,6 +14,7 @@ export default {
GlAlert, GlAlert,
GlLink, GlLink,
GlSprintf, GlSprintf,
GlEmptyState,
SubscriptionsList, SubscriptionsList,
AddNamespaceButton, AddNamespaceButton,
SignInButton, SignInButton,
@ -21,12 +23,18 @@ export default {
usersPath: { usersPath: {
default: '', default: '',
}, },
subscriptions: {
default: [],
},
}, },
computed: { computed: {
...mapState(['alert']), ...mapState(['alert']),
shouldShowAlert() { shouldShowAlert() {
return Boolean(this.alert?.message); return Boolean(this.alert?.message);
}, },
hasSubscriptions() {
return !isEmpty(this.subscriptions);
},
userSignedIn() { userSignedIn() {
return Boolean(!this.usersPath); return Boolean(!this.usersPath);
}, },
@ -66,15 +74,44 @@ export default {
</template> </template>
</gl-alert> </gl-alert>
<h2 class="gl-text-center">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2>
<div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7">
<template v-if="hasSubscriptions">
<div class="gl-display-flex gl-justify-content-end">
<sign-in-button v-if="!userSignedIn" :users-path="usersPath" />
<add-namespace-button v-else />
</div>
<div class="jira-connect-app-body gl-my-7 gl-px-5 gl-pb-4"> <subscriptions-list />
<div class="gl-display-flex gl-justify-content-end"> </template>
<sign-in-button v-if="!userSignedIn" :users-path="usersPath" /> <template v-else>
<add-namespace-button v-else /> <div v-if="!userSignedIn" class="gl-text-center">
</div> <p class="gl-mb-7">{{ s__('JiraService|Sign in to GitLab.com to get started.') }}</p>
<sign-in-button class="gl-mb-7" :users-path="usersPath">
<subscriptions-list /> {{ __('Sign in to GitLab') }}
</sign-in-button>
<p>
{{
s__(
'Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).',
)
}}
</p>
</div>
<gl-empty-state
v-else
:title="s__('Integrations|No linked namespaces')"
:description="
s__(
'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
)
"
>
<template #actions>
<add-namespace-button />
</template>
</gl-empty-state>
</template>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,5 +1,5 @@
<script> <script>
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; import { GlButton, GlTable } from '@gitlab/ui';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { mapMutations } from 'vuex'; import { mapMutations } from 'vuex';
import { removeSubscription } from '~/jira_connect/subscriptions/api'; import { removeSubscription } from '~/jira_connect/subscriptions/api';
@ -12,7 +12,6 @@ import GroupItemName from './group_item_name.vue';
export default { export default {
components: { components: {
GlButton, GlButton,
GlEmptyState,
GlTable, GlTable,
GroupItemName, GroupItemName,
TimeagoTooltip, TimeagoTooltip,
@ -44,17 +43,15 @@ export default {
}, },
], ],
i18n: { i18n: {
emptyTitle: s__('Integrations|No linked namespaces'),
emptyDescription: s__(
'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.',
),
unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'), unlinkError: s__('Integrations|Failed to unlink namespace. Please try again.'),
}, },
methods: { methods: {
...mapMutations({ ...mapMutations({
setAlert: SET_ALERT, setAlert: SET_ALERT,
}), }),
isEmpty, isUnlinkButtonDisabled(item) {
return !isEmpty(item);
},
isLoadingItem(item) { isLoadingItem(item) {
return this.loadingItem === item; return this.loadingItem === item;
}, },
@ -81,29 +78,22 @@ export default {
</script> </script>
<template> <template>
<div> <gl-table :items="subscriptions" :fields="$options.fields">
<gl-empty-state <template #cell(name)="{ item }">
v-if="isEmpty(subscriptions)" <group-item-name :group="item.group" />
:title="$options.i18n.emptyTitle" </template>
:description="$options.i18n.emptyDescription" <template #cell(created_at)="{ item }">
/> <timeago-tooltip :time="item.created_at" />
<gl-table v-else :items="subscriptions" :fields="$options.fields"> </template>
<template #cell(name)="{ item }"> <template #cell(actions)="{ item }">
<group-item-name :group="item.group" /> <gl-button
</template> :class="unlinkBtnClass(item)"
<template #cell(created_at)="{ item }"> category="secondary"
<timeago-tooltip :time="item.created_at" /> :loading="isLoadingItem(item)"
</template> :disabled="isUnlinkButtonDisabled(loadingItem)"
<template #cell(actions)="{ item }"> @click.prevent="onClick(item)"
<gl-button >{{ __('Unlink') }}</gl-button
:class="unlinkBtnClass(item)" >
category="secondary" </template>
:loading="isLoadingItem(item)" </gl-table>
:disabled="!isEmpty(loadingItem)"
@click.prevent="onClick(item)"
>{{ __('Unlink') }}</gl-button
>
</template>
</gl-table>
</div>
</template> </template>

View File

@ -10,8 +10,8 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import getStateQuery from '../../queries/get_state.query.graphql'; import getStateQuery from '../../queries/get_state.query.graphql';
import workInProgressQuery from '../../queries/states/work_in_progress.query.graphql'; import draftQuery from '../../queries/states/draft.query.graphql';
import removeWipMutation from '../../queries/toggle_wip.mutation.graphql'; import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
import StatusIcon from '../mr_widget_status_icon.vue'; import StatusIcon from '../mr_widget_status_icon.vue';
export default { export default {
@ -23,7 +23,7 @@ export default {
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: { apollo: {
userPermissions: { userPermissions: {
query: workInProgressQuery, query: draftQuery,
skip() { skip() {
return !this.glFeatures.mergeRequestWidgetGraphql; return !this.glFeatures.mergeRequestWidgetGraphql;
}, },
@ -53,25 +53,25 @@ export default {
}, },
}, },
methods: { methods: {
removeWipMutation() { removeDraftMutation() {
const { mergeRequestQueryVariables } = this; const { mergeRequestQueryVariables } = this;
this.isMakingRequest = true; this.isMakingRequest = true;
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: removeWipMutation, mutation: removeDraftMutation,
variables: { variables: {
...mergeRequestQueryVariables, ...mergeRequestQueryVariables,
wip: false, draft: false,
}, },
update( update(
store, store,
{ {
data: { data: {
mergeRequestSetWip: { mergeRequestSetDraft: {
errors, errors,
mergeRequest: { mergeableDiscussionsState, workInProgress, title }, mergeRequest: { mergeableDiscussionsState, draft, title },
}, },
}, },
}, },
@ -91,7 +91,7 @@ export default {
const data = produce(sourceData, (draftState) => { const data = produce(sourceData, (draftState) => {
draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState; draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState;
draftState.project.mergeRequest.workInProgress = workInProgress; draftState.project.mergeRequest.draft = draft;
draftState.project.mergeRequest.title = title; draftState.project.mergeRequest.title = title;
}); });
@ -104,14 +104,14 @@ export default {
optimisticResponse: { optimisticResponse: {
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation', __typename: 'Mutation',
mergeRequestSetWip: { mergeRequestSetDraft: {
__typename: 'MergeRequestSetWipPayload', __typename: 'MergeRequestSetWipPayload',
errors: [], errors: [],
mergeRequest: { mergeRequest: {
__typename: 'MergeRequest', __typename: 'MergeRequest',
mergeableDiscussionsState: true, mergeableDiscussionsState: true,
title: this.mr.title, title: this.mr.title,
workInProgress: false, draft: false,
}, },
}, },
}, },
@ -119,7 +119,7 @@ export default {
.then( .then(
({ ({
data: { data: {
mergeRequestSetWip: { mergeRequestSetDraft: {
mergeRequest: { title }, mergeRequest: { title },
}, },
}, },
@ -137,9 +137,9 @@ export default {
this.isMakingRequest = false; this.isMakingRequest = false;
}); });
}, },
handleRemoveWIP() { handleRemoveDraft() {
if (this.glFeatures.mergeRequestWidgetGraphql) { if (this.glFeatures.mergeRequestWidgetGraphql) {
this.removeWipMutation(); this.removeDraftMutation();
} else { } else {
this.isMakingRequest = true; this.isMakingRequest = true;
this.service this.service
@ -178,8 +178,8 @@ export default {
size="small" size="small"
:disabled="isMakingRequest" :disabled="isMakingRequest"
:loading="isMakingRequest" :loading="isMakingRequest"
class="js-remove-wip gl-ml-3" class="js-remove-draft gl-ml-3"
@click="handleRemoveWIP" @click="handleRemoveDraft"
> >
{{ s__('mrWidget|Mark as ready') }} {{ s__('mrWidget|Mark as ready') }}
</gl-button> </gl-button>

View File

@ -23,7 +23,7 @@ query getState($projectPath: ID!, $iid: String!) {
userPermissions { userPermissions {
canMerge canMerge
} }
workInProgress draft
} }
} }
} }

View File

@ -0,0 +1,10 @@
mutation toggleDraftStatus($projectPath: ID!, $iid: String!, $draft: Boolean!) {
mergeRequestSetDraft(input: { projectPath: $projectPath, iid: $iid, draft: $draft }) {
mergeRequest {
mergeableDiscussionsState
title
draft
}
errors
}
}

View File

@ -1,10 +0,0 @@
mutation toggleWIPStatus($projectPath: ID!, $iid: String!, $wip: Boolean!) {
mergeRequestSetWip(input: { projectPath: $projectPath, iid: $iid, wip: $wip }) {
mergeRequest {
mergeableDiscussionsState
title
workInProgress
}
errors
}
}

View File

@ -17,8 +17,8 @@ export default function deviseState() {
return stateKey.rebase; return stateKey.rebase;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) { } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return stateKey.pipelineFailed; return stateKey.pipelineFailed;
} else if (this.workInProgress) { } else if (this.draft) {
return stateKey.workInProgress; return stateKey.draft;
} else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) { } else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
return stateKey.unresolvedDiscussions; return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) { } else if (this.isPipelineBlocked) {

View File

@ -164,7 +164,7 @@ export default class MergeRequestStore {
this.projectArchived = data.project_archived; this.projectArchived = data.project_archived;
this.isSHAMismatch = this.sha !== data.diff_head_sha; this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.shouldBeRebased = Boolean(data.should_be_rebased); this.shouldBeRebased = Boolean(data.should_be_rebased);
this.workInProgress = data.work_in_progress; this.draft = data.draft;
} }
const currentUser = data.current_user; const currentUser = data.current_user;
@ -207,7 +207,7 @@ export default class MergeRequestStore {
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled'; this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha; this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
this.shouldBeRebased = mergeRequest.shouldBeRebased; this.shouldBeRebased = mergeRequest.shouldBeRebased;
this.workInProgress = mergeRequest.workInProgress; this.draft = mergeRequest.draft;
this.mergeRequestState = mergeRequest.state; this.mergeRequestState = mergeRequest.state;
this.setState(); this.setState();

View File

@ -4,7 +4,7 @@ export const stateToComponentMap = {
merging: 'mr-widget-merging', merging: 'mr-widget-merging',
conflicts: 'mr-widget-conflicts', conflicts: 'mr-widget-conflicts',
missingBranch: 'mr-widget-missing-branch', missingBranch: 'mr-widget-missing-branch',
workInProgress: 'mr-widget-wip', draft: 'mr-widget-wip',
readyToMerge: 'mr-widget-ready-to-merge', readyToMerge: 'mr-widget-ready-to-merge',
nothingToMerge: 'mr-widget-nothing-to-merge', nothingToMerge: 'mr-widget-nothing-to-merge',
notAllowedToMerge: 'mr-widget-not-allowed', notAllowedToMerge: 'mr-widget-not-allowed',
@ -24,7 +24,7 @@ export const stateToComponentMap = {
export const statesToShowHelpWidget = [ export const statesToShowHelpWidget = [
'merging', 'merging',
'conflicts', 'conflicts',
'workInProgress', 'draft',
'readyToMerge', 'readyToMerge',
'checking', 'checking',
'unresolvedDiscussions', 'unresolvedDiscussions',
@ -40,7 +40,7 @@ export const stateKey = {
nothingToMerge: 'nothingToMerge', nothingToMerge: 'nothingToMerge',
checking: 'checking', checking: 'checking',
conflicts: 'conflicts', conflicts: 'conflicts',
workInProgress: 'workInProgress', draft: 'draft',
pipelineFailed: 'pipelineFailed', pipelineFailed: 'pipelineFailed',
unresolvedDiscussions: 'unresolvedDiscussions', unresolvedDiscussions: 'unresolvedDiscussions',
pipelineBlocked: 'pipelineBlocked', pipelineBlocked: 'pipelineBlocked',

View File

@ -42,8 +42,6 @@ $header-height: 40px;
.jira-connect-app-body { .jira-connect-app-body {
max-width: 768px; max-width: 768px;
margin-left: auto;
margin-right: auto;
} }
// needed for external_link // needed for external_link

View File

@ -113,8 +113,6 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end end
def send_manifest(manifest, from_cache:) def send_manifest(manifest, from_cache:)
# Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
manifest.touch
response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest response.headers[DependencyProxy::Manifest::DIGEST_HEADER] = manifest.digest
response.headers['Content-Length'] = manifest.size response.headers['Content-Length'] = manifest.size
response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION

View File

@ -1,35 +0,0 @@
# frozen_string_literal: true
module Mutations
module MergeRequests
class SetWip < Base
graphql_name 'MergeRequestSetWip'
argument :wip,
GraphQL::Types::Boolean,
required: true,
description: <<~DESC
Whether or not to set the merge request as a draft.
DESC
def resolve(project_path:, iid:, wip: nil)
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
::MergeRequests::UpdateService.new(project: project, current_user: current_user, params: { wip_event: wip_event(merge_request, wip) })
.execute(merge_request)
{
merge_request: merge_request,
errors: errors_on_object(merge_request)
}
end
private
def wip_event(merge_request, wip)
wip ? 'wip' : 'unwip'
end
end
end
end

View File

@ -53,9 +53,6 @@ module Types
description: 'Indicates if the source branch is protected.' description: 'Indicates if the source branch is protected.'
field :target_branch, GraphQL::Types::String, null: false, field :target_branch, GraphQL::Types::String, null: false,
description: 'Target branch of the merge request.' description: 'Target branch of the merge request.'
field :work_in_progress, GraphQL::Types::Boolean, method: :work_in_progress?, null: false,
deprecated: { reason: 'Use `draft`', milestone: '13.12' },
description: 'Indicates if the merge request is a draft.'
field :draft, GraphQL::Types::Boolean, method: :draft?, null: false, field :draft, GraphQL::Types::Boolean, method: :draft?, null: false,
description: 'Indicates if the merge request is a draft.' description: 'Indicates if the merge request is a draft.'
field :merge_when_pipeline_succeeds, GraphQL::Types::Boolean, null: true, field :merge_when_pipeline_succeeds, GraphQL::Types::Boolean, null: true,

View File

@ -65,9 +65,6 @@ module Types
mount_mutation Mutations::MergeRequests::SetLocked mount_mutation Mutations::MergeRequests::SetLocked
mount_mutation Mutations::MergeRequests::SetMilestone mount_mutation Mutations::MergeRequests::SetMilestone
mount_mutation Mutations::MergeRequests::SetSubscription mount_mutation Mutations::MergeRequests::SetSubscription
mount_mutation Mutations::MergeRequests::SetWip,
calls_gitaly: true,
deprecated: { reason: 'Use mergeRequestSetDraft', milestone: '13.12' }
mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetDraft, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::MergeRequests::ReviewerRereview mount_mutation Mutations::MergeRequests::ReviewerRereview

View File

@ -5,10 +5,11 @@ module TtlExpirable
included do included do
validates :status, presence: true validates :status, presence: true
default_value_for :read_at, Time.zone.now
enum status: { default: 0, expired: 1, processing: 2, error: 3 } enum status: { default: 0, expired: 1, processing: 2, error: 3 }
scope :updated_before, ->(number_of_days) { where("updated_at <= ?", Time.zone.now - number_of_days.days) } scope :read_before, ->(number_of_days) { where("read_at <= ?", Time.zone.now - number_of_days.days) }
scope :active, -> { where(status: :default) } scope :active, -> { where(status: :default) }
scope :lock_next_by, ->(sort) do scope :lock_next_by, ->(sort) do
@ -17,4 +18,8 @@ module TtlExpirable
.lock('FOR UPDATE SKIP LOCKED') .lock('FOR UPDATE SKIP LOCKED')
end end
end end
def read!
self.update(read_at: Time.zone.now)
end
end end

View File

@ -12,7 +12,8 @@ class GpgSignature < ApplicationRecord
same_user_different_email: 2, same_user_different_email: 2,
other_user: 3, other_user: 3,
unverified_key: 4, unverified_key: 4,
unknown_key: 5 unknown_key: 5,
multiple_signatures: 6
} }
belongs_to :project belongs_to :project

View File

@ -268,7 +268,6 @@ class MergeRequest < ApplicationRecord
from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id) from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id)
end end
scope :merged, -> { with_state(:merged) } scope :merged, -> { with_state(:merged) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
scope :open_and_closed, -> { with_states(:opened, :closed) } scope :open_and_closed, -> { with_states(:opened, :closed) }
scope :drafts, -> { where(draft: true) } scope :drafts, -> { where(draft: true) }
scope :from_source_branches, ->(branches) { where(source_branch: branches) } scope :from_source_branches, ->(branches) { where(source_branch: branches) }

View File

@ -497,6 +497,10 @@ class Namespace < ApplicationRecord
Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml) Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
end end
def project_namespace_creation_enabled?
Feature.enabled?(:create_project_namespace_on_project_create, self, default_enabled: :yaml)
end
private private
def expire_child_caches def expire_child_caches

View File

@ -4,8 +4,6 @@ module Namespaces
class ProjectNamespace < Namespace class ProjectNamespace < Namespace
has_one :project, foreign_key: :project_namespace_id, inverse_of: :project_namespace has_one :project, foreign_key: :project_namespace_id, inverse_of: :project_namespace
validates :project, presence: true
def self.sti_name def self.sti_name
'Project' 'Project'
end end

View File

@ -9,5 +9,9 @@ module Packages
package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
end end
def self.table_name_prefix
'packages_npm_'
end
end end
end end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class Packages::Npm::Metadatum < ApplicationRecord
belongs_to :package, -> { where(package_type: :npm) }, inverse_of: :npm_metadatum
validates :package, presence: true
# From https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
validates :package_json, json_schema: { filename: "npm_package_json" }
validate :ensure_npm_package_type
validate :ensure_package_json_size
private
def ensure_npm_package_type
return if package&.npm?
errors.add(:base, _('Package type must be NPM'))
end
def ensure_package_json_size
return if package_json.to_s.size < 20000
errors.add(:package_json, _('structure is too large'))
end
end

View File

@ -39,6 +39,7 @@ class Packages::Package < ApplicationRecord
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum' has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum' has_one :composer_metadatum, inverse_of: :package, class_name: 'Packages::Composer::Metadatum'
has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum' has_one :rubygems_metadatum, inverse_of: :package, class_name: 'Packages::Rubygems::Metadatum'
has_one :npm_metadatum, inverse_of: :package, class_name: 'Packages::Npm::Metadatum'
has_many :build_infos, inverse_of: :package has_many :build_infos, inverse_of: :package
has_many :pipelines, through: :build_infos, disable_joins: -> { disable_cross_joins_to_pipelines? } has_many :pipelines, through: :build_infos, disable_joins: -> { disable_cross_joins_to_pipelines? }
has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication' has_one :debian_publication, inverse_of: :package, class_name: 'Packages::Debian::Publication'
@ -126,6 +127,7 @@ class Packages::Package < ApplicationRecord
.where(Packages::Composer::Metadatum.table_name => { target_sha: target }) .where(Packages::Composer::Metadatum.table_name => { target_sha: target })
end end
scope :preload_composer, -> { preload(:composer_metadatum) } scope :preload_composer, -> { preload(:composer_metadatum) }
scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }

View File

@ -98,7 +98,7 @@ class Project < ApplicationRecord
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
before_save :ensure_runners_token before_save :ensure_runners_token
before_save :ensure_project_namespace_in_sync before_validation :ensure_project_namespace_in_sync
after_save :update_project_statistics, if: :saved_change_to_namespace_id? after_save :update_project_statistics, if: :saved_change_to_namespace_id?
@ -147,7 +147,7 @@ class Project < ApplicationRecord
belongs_to :namespace belongs_to :namespace
# Sync deletion via DB Trigger to ensure we do not have # Sync deletion via DB Trigger to ensure we do not have
# a project without a project_namespace (or vice-versa) # a project without a project_namespace (or vice-versa)
belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id'
alias_method :parent, :namespace alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id alias_attribute :parent_id, :namespace_id
@ -476,6 +476,7 @@ class Project < ApplicationRecord
validates :project_feature, presence: true validates :project_feature, presence: true
validates :namespace, presence: true validates :namespace, presence: true
validates :project_namespace, presence: true, if: -> { self.namespace && self.root_namespace.project_namespace_creation_enabled? }
validates :name, uniqueness: { scope: :namespace_id } validates :name, uniqueness: { scope: :namespace_id }
validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS }, validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS }, ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
@ -2919,12 +2920,28 @@ class Project < ApplicationRecord
end end
def ensure_project_namespace_in_sync def ensure_project_namespace_in_sync
if changes.keys & [:name, :path, :namespace_id, :visibility_level] && project_namespace.present? # create project_namespace when project is created if create_project_namespace_on_project_create FF is enabled
project_namespace.name = name build_project_namespace if project_namespace_creation_enabled?
project_namespace.path = path
project_namespace.parent = namespace # regardless of create_project_namespace_on_project_create FF we need
project_namespace.visibility_level = visibility_level # to keep project and project namespace in sync if there is one
end sync_attributes(project_namespace) if sync_project_namespace?
end
def project_namespace_creation_enabled?
new_record? && !project_namespace && self.namespace && self.root_namespace.project_namespace_creation_enabled?
end
def sync_project_namespace?
(changes.keys & %w(name path namespace_id namespace visibility_level shared_runners_enabled)).any? && project_namespace.present?
end
def sync_attributes(project_namespace)
project_namespace.name = name
project_namespace.path = path
project_namespace.parent = namespace
project_namespace.shared_runners_enabled = shared_runners_enabled
project_namespace.visibility_level = visibility_level
end end
end end

View File

@ -1091,6 +1091,13 @@ class Repository
after_create after_create
true true
rescue Gitlab::Git::Repository::RepositoryExists
# We do not want to call `#after_create` given that we didn't create the
# repo, but we obviously have a mismatch between what's in our exists cache
# and actual on-disk state as seen by Gitaly. Let's thus expire our caches.
expire_status_cache
nil
end end
def create_from_bundle(bundle_path) def create_from_bundle(bundle_path)

View File

@ -5,26 +5,37 @@ module Packages
class PackagePresenter class PackagePresenter
include API::Helpers::RelatedResourcesHelpers include API::Helpers::RelatedResourcesHelpers
# Allowed fields are those defined in the abbreviated form
# defined here: https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
# except: name, version, dist, dependencies and xDependencies. Those are generated by this presenter.
PACKAGE_JSON_ALLOWED_FIELDS = %w[deprecated bin directories dist engines _hasShrinkwrap].freeze
attr_reader :name, :packages attr_reader :name, :packages
def initialize(name, packages) def initialize(name, packages, include_metadata: false)
@name = name @name = name
@packages = packages @packages = packages
@include_metadata = include_metadata
end end
def versions def versions
package_versions = {} package_versions = {}
packages.each_batch do |relation| packages.each_batch do |relation|
relation.including_dependency_links batched_packages = relation.including_dependency_links
.preload_files .preload_files
.each do |package|
package_file = package.package_files.last
next unless package_file if @include_metadata
batched_packages = batched_packages.preload_npm_metadatum
end
package_versions[package.version] = build_package_version(package, package_file) batched_packages.each do |package|
end package_file = package.package_files.last
next unless package_file
package_versions[package.version] = build_package_version(package, package_file)
end
end end
package_versions package_versions
@ -41,14 +52,14 @@ module Packages
end end
def build_package_version(package, package_file) def build_package_version(package, package_file)
{ abbreviated_package_json(package).merge(
name: package.name, name: package.name,
version: package.version, version: package.version,
dist: { dist: {
shasum: package_file.file_sha1, shasum: package_file.file_sha1,
tarball: tarball_url(package, package_file) tarball: tarball_url(package, package_file)
} }
}.tap do |package_version| ).tap do |package_version|
package_version.merge!(build_package_dependencies(package)) package_version.merge!(build_package_dependencies(package))
end end
end end
@ -79,6 +90,13 @@ module Packages
Packages::Tag.for_packages(packages) Packages::Tag.for_packages(packages)
.preload_package .preload_package
end end
def abbreviated_package_json(package)
return {} unless @include_metadata
json = package.npm_metadatum&.package_json || {}
json.slice(*PACKAGE_JSON_ALLOWED_FIELDS)
end
end end
end end
end end

View File

@ -30,8 +30,7 @@ module DependencyProxy
blob.save! blob.save!
end end
# Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536 blob.read! if from_cache
blob.touch if from_cache
success(blob: blob, from_cache: from_cache) success(blob: blob, from_cache: from_cache)
end end

View File

@ -58,6 +58,8 @@ module DependencyProxy
def respond(from_cache: true) def respond(from_cache: true)
if @manifest if @manifest
@manifest.read!
success(manifest: @manifest, from_cache: from_cache) success(manifest: @manifest, from_cache: from_cache)
else else
error('Failed to download the manifest from the external registry', 503) error('Failed to download the manifest from the external registry', 503)

View File

@ -21,6 +21,10 @@ module Packages
::Packages::CreateDependencyService.new(package, package_dependencies).execute ::Packages::CreateDependencyService.new(package, package_dependencies).execute
::Packages::Npm::CreateTagService.new(package, dist_tag).execute ::Packages::Npm::CreateTagService.new(package, dist_tag).execute
if Feature.enabled?(:packages_npm_abbreviated_metadata, project)
package.create_npm_metadatum!(package_json: version_data)
end
package package
end end

View File

@ -152,14 +152,12 @@ module Projects
deleted_count = project.commit_statuses.delete_all deleted_count = project.commit_statuses.delete_all
if deleted_count > 0 Gitlab::AppLogger.info(
Gitlab::AppLogger.info( class: 'Projects::DestroyService',
class: 'Projects::DestroyService', project_id: project.id,
project_id: project.id, message: 'leftover commit statuses',
message: 'leftover commit statuses', orphaned_commit_status_count: deleted_count
orphaned_commit_status_count: deleted_count )
)
end
end end
# The project can have multiple webhooks with hundreds of thousands of web_hook_logs. # The project can have multiple webhooks with hundreds of thousands of web_hook_logs.

View File

@ -0,0 +1,26 @@
{
"description": "NPM package json metadata",
"type": "object",
"properties": {
"name": { "type": "string" },
"version": { "type": "string" },
"dist": {
"type": "object",
"properties": {
"tarball": { "type": "string" },
"shasum": { "type": "string" }
},
"additionalProperties": true,
"required": [
"tarball",
"shasum"
]
}
},
"additionalProperties": true,
"required": [
"name",
"version",
"dist"
]
}

View File

@ -23,7 +23,7 @@
= form_tag personal_access_token_import_github_path, method: :post do = form_tag personal_access_token_import_github_path, method: :post do
.form-group .form-group
%label.label-bold= _('Personal Access Token') %label.label-bold= _('Personal Access Token')
= text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' } = text_field_tag :personal_access_token, '', class: 'form-control gl-form-input', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' }, data: { qa_selector: 'personal_access_token_field' }
%span.form-text.text-muted %span.form-text.text-muted
= import_github_personal_access_token_message = import_github_personal_access_token_message

View File

@ -9,20 +9,9 @@
= link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in' = link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in'
%main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto %main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto
- if current_user.blank? && @subscriptions.empty? .js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
.jira-connect-app-body.gl-px-5.gl-text-center
%h2= s_('JiraService|GitLab for Jira Configuration')
%p= s_('JiraService|Sign in to GitLab.com to get started.')
.gl-mt-7 %p.jira-connect-app-body.gl-px-5.gl-font-base.gl-text-center.gl-mx-auto
= external_link _('Sign in to GitLab'), jira_connect_users_path, class: "btn gl-button btn-confirm js-jira-connect-sign-in"
.gl-mt-7
%p= s_('Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).')
- else
.js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
%p.jira-connect-app-body.gl-px-5.gl-mt-7.gl-font-base.gl-text-center
%strong= s_('Integrations|Browser limitations') %strong= s_('Integrations|Browser limitations')
- browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">' - browser_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'
- firefox_link_start = browser_link_start.html_safe % { url: 'https://www.mozilla.org/en-US/firefox/' } - firefox_link_start = browser_link_start.html_safe % { url: 'https://www.mozilla.org/en-US/firefox/' }

View File

@ -0,0 +1,4 @@
= javascript_tag(nonce: content_security_policy_nonce) do
:plain
gl = window.gl || {};
gl.experiments = #{raw ApplicationExperiment.published_experiments.reject { |name, data| data[:excluded] }.to_json};

View File

@ -16,4 +16,5 @@
= render 'layouts/img_loader' = render 'layouts/img_loader'
= render 'layouts/published_experiments'
= yield :scripts_body = yield :scripts_body

View File

@ -0,0 +1,6 @@
- title = capture do
= html_escape(_('This commit was signed with %{strong_open}multiple%{strong_close} signatures.')) % { strong_open: '<strong>'.html_safe, strong_close: '</strong>'.html_safe }
- locals = { signature: signature, title: title, label: _('Unverified'), css_class: 'invalid', icon: 'status_notfound_borderless' }
= render partial: 'projects/commit/signature_badge', locals: locals

View File

@ -1,5 +1,4 @@
- add_page_startup_api_call discussions_path(@issue) - add_page_startup_api_call discussions_path(@issue)
- add_page_startup_api_call notes_url
- @gfm_form = true - @gfm_form = true

View File

@ -246,6 +246,15 @@
:weight: 1 :weight: 1
:idempotent: true :idempotent: true
:tags: [] :tags: []
- :name: cronjob:clusters_integrations_check_prometheus_health
:worker_name: Clusters::Integrations::CheckPrometheusHealthWorker
:feature_category: :incident_management
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: cronjob:container_expiration_policy - :name: cronjob:container_expiration_policy
:worker_name: ContainerExpirationPolicyWorker :worker_name: ContainerExpirationPolicyWorker
:feature_category: :container_registry :feature_category: :container_registry
@ -1087,15 +1096,6 @@
:idempotent: :idempotent:
:tags: :tags:
- :needs_own_queue - :needs_own_queue
- :name: incident_management:clusters_integrations_check_prometheus_health
:worker_name: Clusters::Integrations::CheckPrometheusHealthWorker
:feature_category: :incident_management
:has_external_dependencies: true
:urgency: :low
:resource_boundary: :unknown
:weight: 2
:idempotent: true
:tags: []
- :name: incident_management:incident_management_add_severity_system_note - :name: incident_management:incident_management_add_severity_system_note
:worker_name: IncidentManagement::AddSeveritySystemNoteWorker :worker_name: IncidentManagement::AddSeveritySystemNoteWorker
:feature_category: :incident_management :feature_category: :incident_management

View File

@ -12,7 +12,6 @@ module Clusters
include CronjobQueue include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext # rubocop:enable Scalability/CronWorkerContext
queue_namespace :incident_management
feature_category :incident_management feature_category :incident_management
urgency :low urgency :low

View File

@ -13,9 +13,8 @@ module DependencyProxy
def perform def perform
DependencyProxy::ImageTtlGroupPolicy.enabled.each do |policy| DependencyProxy::ImageTtlGroupPolicy.enabled.each do |policy|
# Technical Debt: change to read_before https://gitlab.com/gitlab-org/gitlab/-/issues/341536 qualified_blobs = policy.group.dependency_proxy_blobs.active.read_before(policy.ttl)
qualified_blobs = policy.group.dependency_proxy_blobs.active.updated_before(policy.ttl) qualified_manifests = policy.group.dependency_proxy_manifests.active.read_before(policy.ttl)
qualified_manifests = policy.group.dependency_proxy_manifests.active.updated_before(policy.ttl)
enqueue_blob_cleanup_job if expire_artifacts(qualified_blobs, DependencyProxy::Blob) enqueue_blob_cleanup_job if expire_artifacts(qualified_blobs, DependencyProxy::Blob)
enqueue_manifest_cleanup_job if expire_artifacts(qualified_manifests, DependencyProxy::Manifest) enqueue_manifest_cleanup_job if expire_artifacts(qualified_manifests, DependencyProxy::Manifest)

View File

@ -0,0 +1,8 @@
---
name: create_project_namespace_on_project_create
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70972
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344954
milestone: '14.5'
type: development
group: group::workspace
default_enabled: false

View File

@ -1,8 +1,8 @@
--- ---
name: rate_limiter_safe_increment name: multiple_gpg_signatures
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73343 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74095
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285352 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345261
milestone: '14.5' milestone: '14.5'
type: development type: development
group: group::project management group: group::source code
default_enabled: false default_enabled: false

View File

@ -1,8 +1,8 @@
--- ---
name: linear_group_plans_preloaded_ancestor_scopes name: packages_npm_abbreviated_metadata
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70685 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73639
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341349 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344827
milestone: '14.4' milestone: '14.5'
type: development type: development
group: group::access group: group::package
default_enabled: false default_enabled: false

View File

@ -591,6 +591,9 @@ Settings.cron_jobs['batched_background_migrations_worker']['job_class'] = 'Datab
Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances'] ||= Settingslogic.new({}) Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances'] ||= Settingslogic.new({})
Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances']['cron'] ||= '* 0/15 * * *' Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances']['cron'] ||= '* 0/15 * * *'
Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances']['job_class'] = 'Issues::RescheduleStuckIssueRebalancesWorker' Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances']['job_class'] = 'Issues::RescheduleStuckIssueRebalancesWorker'
Settings.cron_jobs['clusters_integrations_check_prometheus_health_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['clusters_integrations_check_prometheus_health_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['clusters_integrations_check_prometheus_health_worker']['job_class'] = 'Clusters::Integrations::CheckPrometheusHealthWorker'
Gitlab.ee do Gitlab.ee do
Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({})

View File

@ -35,8 +35,4 @@ unless Gitlab.jh?
]) ])
end end
begin Gitlab::Database::Partitioning.sync_partitions_ignore_db_error
Gitlab::Database::Partitioning.sync_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP']
rescue ActiveRecord::ActiveRecordError, PG::Error
# ignore - happens when Rake tasks yet have to create a database, e.g. for testing
end

View File

@ -6,7 +6,7 @@
# and what types we expect those attribute values to be. # and what types we expect those attribute values to be.
# #
# For more information please refer to the handbook documentation here: # For more information please refer to the handbook documentation here:
# {{LINK TBD}} # https://about.gitlab.com/handbook/marketing/blog/release-posts/#deprecations
# #
# Please delete this line and above before submitting your merge request. # Please delete this line and above before submitting your merge request.

View File

@ -157,16 +157,17 @@ class Gitlab::Seeder::CycleAnalytics
end end
def create_new_vsm_project def create_new_vsm_project
namespace = FactoryBot.create(
:group,
name: "Value Stream Management Group #{suffix}",
path: "vsmg-#{suffix}"
)
project = FactoryBot.create( project = FactoryBot.create(
:project, :project,
name: "Value Stream Management Project #{suffix}", name: "Value Stream Management Project #{suffix}",
path: "vsmp-#{suffix}", path: "vsmp-#{suffix}",
creator: admin, creator: admin,
namespace: FactoryBot.create( namespace: namespace
:group,
name: "Value Stream Management Group #{suffix}",
path: "vsmg-#{suffix}"
)
) )
project.create_repository project.create_repository

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
class CreatePackagesNpmMetadata < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
with_lock_retries do
create_table :packages_npm_metadata, id: false do |t|
t.references :package, primary_key: true, default: nil, index: false, foreign_key: { to_table: :packages_packages, on_delete: :cascade }, type: :bigint
t.jsonb :package_json, default: {}, null: false
t.check_constraint 'char_length(package_json::text) < 20000'
end
end
end
def down
with_lock_retries do
drop_table :packages_npm_metadata
end
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddReadAtToDependencyProxyManifests < Gitlab::Database::Migration[1.0]
def change
add_column :dependency_proxy_manifests, :read_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddReadAtToDependencyProxyBlobs < Gitlab::Database::Migration[1.0]
def change
add_column :dependency_proxy_blobs, :read_at, :datetime_with_timezone, null: false, default: -> { 'NOW()' }
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class UpdateDependencyProxyIndexesWithReadAt < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
NEW_BLOB_INDEX = 'index_dependency_proxy_blobs_on_group_id_status_read_at_id'
OLD_BLOB_INDEX = 'index_dependency_proxy_blobs_on_group_id_status_and_id'
NEW_MANIFEST_INDEX = 'index_dependency_proxy_manifests_on_group_id_status_read_at_id'
OLD_MANIFEST_INDEX = 'index_dependency_proxy_manifests_on_group_id_status_and_id'
def up
add_concurrent_index :dependency_proxy_blobs, [:group_id, :status, :read_at, :id], name: NEW_BLOB_INDEX
add_concurrent_index :dependency_proxy_manifests, [:group_id, :status, :read_at, :id], name: NEW_MANIFEST_INDEX
remove_concurrent_index_by_name :dependency_proxy_blobs, OLD_BLOB_INDEX
remove_concurrent_index_by_name :dependency_proxy_manifests, OLD_MANIFEST_INDEX
end
def down
add_concurrent_index :dependency_proxy_blobs, [:group_id, :status, :id], name: OLD_BLOB_INDEX
add_concurrent_index :dependency_proxy_manifests, [:group_id, :status, :id], name: OLD_MANIFEST_INDEX
remove_concurrent_index_by_name :dependency_proxy_blobs, NEW_BLOB_INDEX
remove_concurrent_index_by_name :dependency_proxy_manifests, NEW_MANIFEST_INDEX
end
end

View File

@ -0,0 +1 @@
50a5c8af2cde1ae79d627f70d3b266488f76f76b481aefca8516db5360cfa843

View File

@ -0,0 +1 @@
13cf3d164d541df48b6d14d7cc1953113476ba8ea5975d7d0c5f84098e2e0e61

View File

@ -0,0 +1 @@
a48f62bed7e4c4a0e69acd3b340065317aff71602e696970276a4e443f1dcabf

View File

@ -0,0 +1 @@
a2556a3d8b21e59caa6cbf7f83d621fef391904d0c13c77c0e5da713a580b4c9

View File

@ -13237,7 +13237,8 @@ CREATE TABLE dependency_proxy_blobs (
file_store integer, file_store integer,
file_name character varying NOT NULL, file_name character varying NOT NULL,
file text NOT NULL, file text NOT NULL,
status smallint DEFAULT 0 NOT NULL status smallint DEFAULT 0 NOT NULL,
read_at timestamp with time zone DEFAULT now() NOT NULL
); );
CREATE SEQUENCE dependency_proxy_blobs_id_seq CREATE SEQUENCE dependency_proxy_blobs_id_seq
@ -13286,6 +13287,7 @@ CREATE TABLE dependency_proxy_manifests (
digest text NOT NULL, digest text NOT NULL,
content_type text, content_type text,
status smallint DEFAULT 0 NOT NULL, status smallint DEFAULT 0 NOT NULL,
read_at timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT check_079b293a7b CHECK ((char_length(file) <= 255)), CONSTRAINT check_079b293a7b CHECK ((char_length(file) <= 255)),
CONSTRAINT check_167a9a8a91 CHECK ((char_length(content_type) <= 255)), CONSTRAINT check_167a9a8a91 CHECK ((char_length(content_type) <= 255)),
CONSTRAINT check_c579e3f586 CHECK ((char_length(file_name) <= 255)), CONSTRAINT check_c579e3f586 CHECK ((char_length(file_name) <= 255)),
@ -17199,6 +17201,12 @@ CREATE SEQUENCE packages_maven_metadata_id_seq
ALTER SEQUENCE packages_maven_metadata_id_seq OWNED BY packages_maven_metadata.id; ALTER SEQUENCE packages_maven_metadata_id_seq OWNED BY packages_maven_metadata.id;
CREATE TABLE packages_npm_metadata (
package_id bigint NOT NULL,
package_json jsonb DEFAULT '{}'::jsonb NOT NULL,
CONSTRAINT chk_rails_e5cbc301ae CHECK ((char_length((package_json)::text) < 20000))
);
CREATE TABLE packages_nuget_dependency_link_metadata ( CREATE TABLE packages_nuget_dependency_link_metadata (
dependency_link_id bigint NOT NULL, dependency_link_id bigint NOT NULL,
target_framework text NOT NULL, target_framework text NOT NULL,
@ -23524,6 +23532,9 @@ ALTER TABLE ONLY packages_helm_file_metadata
ALTER TABLE ONLY packages_maven_metadata ALTER TABLE ONLY packages_maven_metadata
ADD CONSTRAINT packages_maven_metadata_pkey PRIMARY KEY (id); ADD CONSTRAINT packages_maven_metadata_pkey PRIMARY KEY (id);
ALTER TABLE ONLY packages_npm_metadata
ADD CONSTRAINT packages_npm_metadata_pkey PRIMARY KEY (package_id);
ALTER TABLE ONLY packages_nuget_dependency_link_metadata ALTER TABLE ONLY packages_nuget_dependency_link_metadata
ADD CONSTRAINT packages_nuget_dependency_link_metadata_pkey PRIMARY KEY (dependency_link_id); ADD CONSTRAINT packages_nuget_dependency_link_metadata_pkey PRIMARY KEY (dependency_link_id);
@ -25637,13 +25648,13 @@ CREATE UNIQUE INDEX index_dep_prox_manifests_on_group_id_file_name_and_status ON
CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON dependency_proxy_blobs USING btree (group_id, file_name); CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON dependency_proxy_blobs USING btree (group_id, file_name);
CREATE INDEX index_dependency_proxy_blobs_on_group_id_status_and_id ON dependency_proxy_blobs USING btree (group_id, status, id); CREATE INDEX index_dependency_proxy_blobs_on_group_id_status_read_at_id ON dependency_proxy_blobs USING btree (group_id, status, read_at, id);
CREATE INDEX index_dependency_proxy_blobs_on_status ON dependency_proxy_blobs USING btree (status); CREATE INDEX index_dependency_proxy_blobs_on_status ON dependency_proxy_blobs USING btree (status);
CREATE INDEX index_dependency_proxy_group_settings_on_group_id ON dependency_proxy_group_settings USING btree (group_id); CREATE INDEX index_dependency_proxy_group_settings_on_group_id ON dependency_proxy_group_settings USING btree (group_id);
CREATE INDEX index_dependency_proxy_manifests_on_group_id_status_and_id ON dependency_proxy_manifests USING btree (group_id, status, id); CREATE INDEX index_dependency_proxy_manifests_on_group_id_status_read_at_id ON dependency_proxy_manifests USING btree (group_id, status, read_at, id);
CREATE INDEX index_dependency_proxy_manifests_on_status ON dependency_proxy_manifests USING btree (status); CREATE INDEX index_dependency_proxy_manifests_on_status ON dependency_proxy_manifests USING btree (status);
@ -30779,6 +30790,9 @@ ALTER TABLE ONLY atlassian_identities
ALTER TABLE ONLY serverless_domain_cluster ALTER TABLE ONLY serverless_domain_cluster
ADD CONSTRAINT fk_rails_c09009dee1 FOREIGN KEY (pages_domain_id) REFERENCES pages_domains(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_c09009dee1 FOREIGN KEY (pages_domain_id) REFERENCES pages_domains(id) ON DELETE CASCADE;
ALTER TABLE ONLY packages_npm_metadata
ADD CONSTRAINT fk_rails_c0e5fce6f3 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
ALTER TABLE ONLY labels ALTER TABLE ONLY labels
ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;

View File

@ -3509,31 +3509,6 @@ Input type: `MergeRequestSetSubscriptionInput`
| <a id="mutationmergerequestsetsubscriptionerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationmergerequestsetsubscriptionerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationmergerequestsetsubscriptionmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. | | <a id="mutationmergerequestsetsubscriptionmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. |
### `Mutation.mergeRequestSetWip`
WARNING:
**Deprecated** in 13.12.
Use mergeRequestSetDraft.
Input type: `MergeRequestSetWipInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationmergerequestsetwipclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationmergerequestsetwipiid"></a>`iid` | [`String!`](#string) | IID of the merge request to mutate. |
| <a id="mutationmergerequestsetwipprojectpath"></a>`projectPath` | [`ID!`](#id) | Project the merge request to mutate is in. |
| <a id="mutationmergerequestsetwipwip"></a>`wip` | [`Boolean!`](#boolean) | Whether or not to set the merge request as a draft. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationmergerequestsetwipclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationmergerequestsetwiperrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationmergerequestsetwipmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request after mutation. |
### `Mutation.mergeRequestUpdate` ### `Mutation.mergeRequestUpdate`
Update attributes of a merge request. Update attributes of a merge request.
@ -11583,7 +11558,6 @@ Maven metadata.
| <a id="mergerequestusernotescount"></a>`userNotesCount` | [`Int`](#int) | User notes count of the merge request. | | <a id="mergerequestusernotescount"></a>`userNotesCount` | [`Int`](#int) | User notes count of the merge request. |
| <a id="mergerequestuserpermissions"></a>`userPermissions` | [`MergeRequestPermissions!`](#mergerequestpermissions) | Permissions for the current user on the resource. | | <a id="mergerequestuserpermissions"></a>`userPermissions` | [`MergeRequestPermissions!`](#mergerequestpermissions) | Permissions for the current user on the resource. |
| <a id="mergerequestweburl"></a>`webUrl` | [`String`](#string) | Web URL of the merge request. | | <a id="mergerequestweburl"></a>`webUrl` | [`String`](#string) | Web URL of the merge request. |
| <a id="mergerequestworkinprogress"></a>`workInProgress` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 13.12. Use `draft`. |
#### Fields with arguments #### Fields with arguments

View File

@ -365,8 +365,7 @@ include: '.gitlab-ci-production.yml'
#### `include:file` #### `include:file`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53903) in GitLab 11.7. > Including multiple files from the same project [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26793) in GitLab 13.6. [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/271560) in GitLab 13.8.
> - Including multiple files from the same project [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26793) in GitLab 13.6. [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/271560) in GitLab 13.8.
To include files from another private project on the same GitLab instance, To include files from another private project on the same GitLab instance,
use `include:file`. You can use `include:file` in combination with `include:project` only. use `include:file`. You can use `include:file` in combination with `include:project` only.
@ -451,8 +450,6 @@ include:
#### `include:template` #### `include:template`
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53445) in GitLab 11.7.
Use `include:template` to include [`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates). Use `include:template` to include [`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
**Keyword type**: Global keyword. **Keyword type**: Global keyword.
@ -643,8 +640,7 @@ job2:
**Additional details**: **Additional details**:
You might need to use single quotes (`'`) or double quotes (`"`) when using - When you use [these special characters in `script`](script.md#use-special-characters-with-script), you must use single quotes (`'`) or double quotes (`"`) .
[special characters in `script`](script.md#use-special-characters-with-script).
**Related topics**: **Related topics**:
@ -680,8 +676,8 @@ job:
**Additional details**: **Additional details**:
Scripts you specify in `before_script` are concatenated with any scripts you specify - Scripts you specify in `before_script` are concatenated with any scripts you specify
in the main [`script`](#script). The combined scripts execute together in a single shell. in the main [`script`](#script). The combined scripts execute together in a single shell.
**Related topics**: **Related topics**:
@ -799,7 +795,7 @@ job4:
Use the `.pre` stage to make a job run at the start of a pipeline. `.pre` is Use the `.pre` stage to make a job run at the start of a pipeline. `.pre` is
always the first stage in a pipeline. User-defined stages execute after `.pre`. always the first stage in a pipeline. User-defined stages execute after `.pre`.
You do not need to define `.pre` in [`stages`](#stages). You do not have to define `.pre` in [`stages`](#stages).
You must have a job in at least one stage other than `.pre` or `.post`. You must have a job in at least one stage other than `.pre` or `.post`.
@ -834,7 +830,7 @@ job2:
Use the `.post` stage to make a job run at the end of a pipeline. `.post` Use the `.post` stage to make a job run at the end of a pipeline. `.post`
is always the last stage in a pipeline. User-defined stages execute before `.post`. is always the last stage in a pipeline. User-defined stages execute before `.post`.
You do not need to define `.post` in [`stages`](#stages). You do not have to define `.post` in [`stages`](#stages).
You must have a job in at least one stage other than `.pre` or `.post`. You must have a job in at least one stage other than `.pre` or `.post`.
@ -865,8 +861,6 @@ job2:
### `extends` ### `extends`
> Introduced in GitLab 11.3.
Use `extends` to reuse configuration sections. It's an alternative to [YAML anchors](yaml_specific_features.md#anchors) Use `extends` to reuse configuration sections. It's an alternative to [YAML anchors](yaml_specific_features.md#anchors)
and is a little more flexible and readable. You can use `extends` to reuse configuration and is a little more flexible and readable. You can use `extends` to reuse configuration
from [included configuration files](#use-extends-and-include-together). from [included configuration files](#use-extends-and-include-together).
@ -1356,7 +1350,7 @@ pipeline based on branch names or pipeline types.
| `schedules` | For [scheduled pipelines](../pipelines/schedules.md). | | `schedules` | For [scheduled pipelines](../pipelines/schedules.md). |
| `tags` | When the Git reference for a pipeline is a tag. | | `tags` | When the Git reference for a pipeline is a tag. |
| `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#authentication-tokens). | | `triggers` | For pipelines created by using a [trigger token](../triggers/index.md#authentication-tokens). |
| `web` | For pipelines created by using **Run pipeline** button in the GitLab UI, from the project's **CI/CD > Pipelines** section. | | `web` | For pipelines created by selecting **Run pipeline** in the GitLab UI, from the project's **CI/CD > Pipelines** section. |
**Example of `only:refs` and `except:refs`**: **Example of `only:refs` and `except:refs`**:
@ -1440,8 +1434,6 @@ deploy:
#### `only:changes` / `except:changes` #### `only:changes` / `except:changes`
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/19232) in GitLab 11.4.
Use the `changes` keyword with `only` to run a job, or with `except` to skip a job, Use the `changes` keyword with `only` to run a job, or with `except` to skip a job,
when a Git push event modifies a file. when a Git push event modifies a file.
@ -2043,7 +2035,7 @@ Use `environment` to define the [environment](../environments/index.md) that a j
**Possible inputs**: The name of the environment the job deploys to, in one of these **Possible inputs**: The name of the environment the job deploys to, in one of these
formats: formats:
- Plain text, including letters, digits, spaces and these characters: `-`, `_`, `/`, `$`, `{`, `}`. - Plain text, including letters, digits, spaces, and these characters: `-`, `_`, `/`, `$`, `{`, `}`.
- CI/CD variables, including predefined, secure, or variables defined in the - CI/CD variables, including predefined, secure, or variables defined in the
`.gitlab-ci.yml` file. You can't use variables defined in a `script` section. `.gitlab-ci.yml` file. You can't use variables defined in a `script` section.
@ -2072,7 +2064,7 @@ Common environment names are `qa`, `staging`, and `production`, but you can use
**Possible inputs**: The name of the environment the job deploys to, in one of these **Possible inputs**: The name of the environment the job deploys to, in one of these
formats: formats:
- Plain text, including letters, digits, spaces and these characters: `-`, `_`, `/`, `$`, `{`, `}`. - Plain text, including letters, digits, spaces, and these characters: `-`, `_`, `/`, `$`, `{`, `}`.
- CI/CD variables, including predefined, secure, or variables defined in the - CI/CD variables, including predefined, secure, or variables defined in the
`.gitlab-ci.yml` file. You can't use variables defined in a `script` section. `.gitlab-ci.yml` file. You can't use variables defined in a `script` section.
@ -2124,7 +2116,7 @@ environment.
**Additional details**: **Additional details**:
See [`environment:action`](#environmentaction) for more details and an example. - See [`environment:action`](#environmentaction) for more details and an example.
#### `environment:action` #### `environment:action`
@ -2392,7 +2384,7 @@ cache-job:
**Additional details**: **Additional details**:
- If you use **Windows Batch** to run your shell scripts you need to replace - If you use **Windows Batch** to run your shell scripts you must replace
`$` with `%`. For example: `key: %CI_COMMIT_REF_SLUG%` `$` with `%`. For example: `key: %CI_COMMIT_REF_SLUG%`
- The `cache:key` value can't contain: - The `cache:key` value can't contain:
@ -2446,9 +2438,11 @@ these files changes, a new cache key is computed and a new cache is created. Any
job runs that use the same `Gemfile.lock` and `package.json` with `cache:key:files` job runs that use the same `Gemfile.lock` and `package.json` with `cache:key:files`
use the new cache, instead of rebuilding the dependencies. use the new cache, instead of rebuilding the dependencies.
**Additional details**: The cache `key` is a SHA computed from the most recent commits **Additional details**:
that changed each listed file. If neither file is changed in any commits, the
fallback key is `default`. - The cache `key` is a SHA computed from the most recent commits
that changed each listed file.
If neither file is changed in any commits, the fallback key is `default`.
##### `cache:key:prefix` ##### `cache:key:prefix`
@ -2485,8 +2479,9 @@ If a branch changes `Gemfile.lock`, that branch has a new SHA checksum for `cach
A new cache key is generated, and a new cache is created for that key. If `Gemfile.lock` A new cache key is generated, and a new cache is created for that key. If `Gemfile.lock`
is not found, the prefix is added to `default`, so the key in the example would be `rspec-default`. is not found, the prefix is added to `default`, so the key in the example would be `rspec-default`.
**Additional details**: If no file in `cache:key:files` is changed in any commits, **Additional details**:
the prefix is added to the `default` key.
- If no file in `cache:key:files` is changed in any commits, the prefix is added to the `default` key.
#### `cache:untracked` #### `cache:untracked`
@ -2552,7 +2547,7 @@ This example stores the cache whether or not the job fails or succeeds.
To change the upload and download behavior of a cache, use the `cache:policy` keyword. To change the upload and download behavior of a cache, use the `cache:policy` keyword.
By default, the job downloads the cache when the job starts, and uploads changes By default, the job downloads the cache when the job starts, and uploads changes
to the cache when the job ends. This is the `pull-push` policy (default). to the cache when the job ends. This caching style is the `pull-push` policy (default).
To set a job to only download the cache when the job starts, but never upload changes To set a job to only download the cache when the job starts, but never upload changes
when the job finishes, use `cache:policy:pull`. when the job finishes, use `cache:policy:pull`.
@ -2766,7 +2761,7 @@ time is not defined, it defaults to the
To override the expiration date and protect artifacts from being automatically deleted: To override the expiration date and protect artifacts from being automatically deleted:
- Use the **Keep** button on the job page. - Select **Keep** on the job page.
- [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of - [In GitLab 13.3 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/22761), set the value of
`expire_in` to `never`. `expire_in` to `never`.
@ -2871,7 +2866,7 @@ job:
--- ---
If you use **Windows Batch** to run your shell scripts you need to replace If you use **Windows Batch** to run your shell scripts you must replace
`$` with `%`: `$` with `%`:
```yaml ```yaml
@ -2882,7 +2877,7 @@ job:
- binaries/ - binaries/
``` ```
If you use **Windows PowerShell** to run your shell scripts you need to replace If you use **Windows PowerShell** to run your shell scripts you must replace
`$` with `$env:`: `$` with `$env:`:
```yaml ```yaml
@ -2900,9 +2895,8 @@ link outside it. You can use Wildcards that use [glob](https://en.wikipedia.org/
patterns and: patterns and:
- In [GitLab Runner 13.0 and later](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620), - In [GitLab Runner 13.0 and later](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/2620),
[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match). [`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match).
- In GitLab Runner 12.10 and earlier, - In GitLab Runner 12.10 and earlier, [`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
[`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
To restrict which jobs a specific job fetches artifacts from, see [dependencies](#dependencies). To restrict which jobs a specific job fetches artifacts from, see [dependencies](#dependencies).
@ -2983,9 +2977,6 @@ artifacts:
#### `artifacts:reports` #### `artifacts:reports`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2.
> - Requires GitLab Runner 11.2 and above.
Use [`artifacts:reports`](#artifactsreports) to: Use [`artifacts:reports`](#artifactsreports) to:
- Collect test reports, code quality reports, and security reports from jobs. - Collect test reports, code quality reports, and security reports from jobs.
@ -3041,9 +3032,7 @@ requests and the pipeline view. It's also used to provide data for security dash
##### `artifacts:reports:browser_performance` **(PREMIUM)** ##### `artifacts:reports:browser_performance` **(PREMIUM)**
> - Introduced in GitLab 11.5. > [Name changed](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) from `artifacts:reports:performance` in GitLab 14.0.
> - Requires GitLab Runner 11.5 and above.
> - [Name changed](https://gitlab.com/gitlab-org/gitlab/-/issues/225914) from `artifacts:reports:performance` in GitLab 14.0.
The `browser_performance` report collects [Browser Performance Testing metrics](../../user/project/merge_requests/browser_performance_testing.md) The `browser_performance` report collects [Browser Performance Testing metrics](../../user/project/merge_requests/browser_performance_testing.md)
as artifacts. as artifacts.
@ -3064,8 +3053,7 @@ dashboards.
##### `artifacts:reports:cobertura` ##### `artifacts:reports:cobertura`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9.
> - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above.
The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md). The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md).
The collected Cobertura coverage reports upload to GitLab as an artifact The collected Cobertura coverage reports upload to GitLab as an artifact
@ -3076,9 +3064,7 @@ third party ports for other languages like JavaScript, Python, Ruby, and so on.
##### `artifacts:reports:codequality` ##### `artifacts:reports:codequality`
> - Introduced in GitLab 11.5. > [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) to GitLab Free in 13.2.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) to GitLab Free in 13.2.
> - Requires GitLab Runner 11.5 and above.
The `codequality` report collects [Code Quality issues](../../user/project/merge_requests/code_quality.md) The `codequality` report collects [Code Quality issues](../../user/project/merge_requests/code_quality.md)
as artifacts. as artifacts.
@ -3087,9 +3073,6 @@ The collected Code Quality report uploads to GitLab as an artifact and is summar
##### `artifacts:reports:container_scanning` **(ULTIMATE)** ##### `artifacts:reports:container_scanning` **(ULTIMATE)**
> - Introduced in GitLab 11.5.
> - Requires GitLab Runner 11.5 and above.
The `container_scanning` report collects [Container Scanning vulnerabilities](../../user/application_security/container_scanning/index.md) The `container_scanning` report collects [Container Scanning vulnerabilities](../../user/application_security/container_scanning/index.md)
as artifacts. as artifacts.
@ -3110,9 +3093,6 @@ requests and the pipeline view. It's also used to provide data for security dash
##### `artifacts:reports:dast` **(ULTIMATE)** ##### `artifacts:reports:dast` **(ULTIMATE)**
> - Introduced in GitLab 11.5.
> - Requires GitLab Runner 11.5 and above.
The `dast` report collects [DAST vulnerabilities](../../user/application_security/dast/index.md) The `dast` report collects [DAST vulnerabilities](../../user/application_security/dast/index.md)
as artifacts. as artifacts.
@ -3121,9 +3101,6 @@ dashboards.
##### `artifacts:reports:dependency_scanning` **(ULTIMATE)** ##### `artifacts:reports:dependency_scanning` **(ULTIMATE)**
> - Introduced in GitLab 11.5.
> - Requires GitLab Runner 11.5 and above.
The `dependency_scanning` report collects [Dependency Scanning vulnerabilities](../../user/application_security/dependency_scanning/index.md) The `dependency_scanning` report collects [Dependency Scanning vulnerabilities](../../user/application_security/dependency_scanning/index.md)
as artifacts. as artifacts.
@ -3132,15 +3109,14 @@ dashboards.
##### `artifacts:reports:dotenv` ##### `artifacts:reports:dotenv`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17066) in GitLab 12.9. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17066) in GitLab 12.9.
> - Requires GitLab Runner 11.5 and later.
The `dotenv` report collects a set of environment variables as artifacts. The `dotenv` report collects a set of environment variables as artifacts.
The collected variables are registered as runtime-created variables of the job, The collected variables are registered as runtime-created variables of the job,
which is useful to [set dynamic environment URLs after a job finishes](../environments/index.md#set-dynamic-environment-urls-after-a-job-finishes). which is useful to [set dynamic environment URLs after a job finishes](../environments/index.md#set-dynamic-environment-urls-after-a-job-finishes).
There are a couple of exceptions to the [original dotenv rules](https://github.com/motdotla/dotenv#rules): The exceptions to the [original dotenv rules](https://github.com/motdotla/dotenv#rules) are:
- The variable key can contain only letters, digits, and underscores (`_`). - The variable key can contain only letters, digits, and underscores (`_`).
- The maximum size of the `.env` file is 5 KB. - The maximum size of the `.env` file is 5 KB.
@ -3154,9 +3130,6 @@ There are a couple of exceptions to the [original dotenv rules](https://github.c
##### `artifacts:reports:junit` ##### `artifacts:reports:junit`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20390) in GitLab 11.2.
> - Requires GitLab Runner 11.2 and above.
The `junit` report collects [JUnit report format XML files](https://www.ibm.com/docs/en/adfz/developer-for-zos/14.1.0?topic=formats-junit-xml-format) The `junit` report collects [JUnit report format XML files](https://www.ibm.com/docs/en/adfz/developer-for-zos/14.1.0?topic=formats-junit-xml-format)
as artifacts. Although JUnit was originally developed in Java, there are many as artifacts. Although JUnit was originally developed in Java, there are many
third party ports for other third party ports for other
@ -3179,21 +3152,20 @@ rspec:
The collected Unit test reports upload to GitLab as an artifact and display in merge requests. The collected Unit test reports upload to GitLab as an artifact and display in merge requests.
If the JUnit tool you use exports to multiple XML files, specify If the JUnit tool you use exports to multiple XML files, specify
multiple test report paths within a single job to multiple test report paths in a single job to
concatenate them into a single file. Use a filename pattern (`junit: rspec-*.xml`), concatenate them into a single file. Use a filename pattern (`junit: rspec-*.xml`),
an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a an array of filenames (`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`), or a
combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`). combination thereof (`junit: [rspec.xml, test-results/TEST-*.xml]`).
##### `artifacts:reports:license_scanning` **(ULTIMATE)** ##### `artifacts:reports:license_scanning` **(ULTIMATE)**
> - Introduced in GitLab 12.8. > Introduced in GitLab 12.8.
> - Requires GitLab Runner 11.5 and above.
The `license_scanning` report collects [Licenses](../../user/compliance/license_compliance/index.md) The `license_scanning` report collects [Licenses](../../user/compliance/license_compliance/index.md)
as artifacts. as artifacts.
The License Compliance report uploads to GitLab as an artifact and displays automatically in merge requests and the pipeline view, and provide data for security The License Compliance report uploads to GitLab as an artifact and displays automatically
dashboards. in merge requests and the pipeline view. The report provides data for security dashboards.
##### `artifacts:reports:load_performance` **(PREMIUM)** ##### `artifacts:reports:load_performance` **(PREMIUM)**
@ -3208,8 +3180,6 @@ shown in merge requests automatically.
##### `artifacts:reports:metrics` **(PREMIUM)** ##### `artifacts:reports:metrics` **(PREMIUM)**
> Introduced in GitLab 11.10.
The `metrics` report collects [Metrics](../metrics_reports.md) The `metrics` report collects [Metrics](../metrics_reports.md)
as artifacts. as artifacts.
@ -3218,7 +3188,6 @@ The collected Metrics report uploads to GitLab as an artifact and displays in me
##### `artifacts:reports:requirements` **(ULTIMATE)** ##### `artifacts:reports:requirements` **(ULTIMATE)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2859) in GitLab 13.1. > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2859) in GitLab 13.1.
> - Requires GitLab Runner 11.5 and above.
The `requirements` report collects `requirements.json` files as artifacts. The `requirements` report collects `requirements.json` files as artifacts.
@ -3228,9 +3197,7 @@ marked as Satisfied.
##### `artifacts:reports:sast` ##### `artifacts:reports:sast`
> - Introduced in GitLab 11.5.
> - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/2098) from GitLab Ultimate to GitLab Free in 13.3. > - [Moved](https://gitlab.com/groups/gitlab-org/-/epics/2098) from GitLab Ultimate to GitLab Free in 13.3.
> - Requires GitLab Runner 11.5 and above.
The `sast` report collects [SAST vulnerabilities](../../user/application_security/sast/index.md) The `sast` report collects [SAST vulnerabilities](../../user/application_security/sast/index.md)
as artifacts. as artifacts.
@ -3302,7 +3269,7 @@ failure.
1. `on_success` (default): Upload artifacts only when the job succeeds. 1. `on_success` (default): Upload artifacts only when the job succeeds.
1. `on_failure`: Upload artifacts only when the job fails. 1. `on_failure`: Upload artifacts only when the job fails.
1. `always`: Always upload artifacts. Useful, for example, when 1. `always`: Always upload artifacts. For example, when
[uploading artifacts](../unit_test_reports.md#viewing-junit-screenshots-on-gitlab) required to [uploading artifacts](../unit_test_reports.md#viewing-junit-screenshots-on-gitlab) required to
troubleshoot failing tests. troubleshoot failing tests.
@ -3394,8 +3361,6 @@ to select a specific site profile and scanner profile.
### `retry` ### `retry`
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/3515) in GitLab 11.5, you can control which failures to retry on.
Use `retry` to configure how many times a job is retried if it fails. Use `retry` to configure how many times a job is retried if it fails.
If not defined, defaults to `0` and jobs do not retry. If not defined, defaults to `0` and jobs do not retry.
@ -3513,8 +3478,6 @@ test:
### `parallel` ### `parallel`
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/21480) in GitLab 11.5.
Use `parallel` to run a job multiple times in parallel in a single pipeline. Use `parallel` to run a job multiple times in parallel in a single pipeline.
Multiple runners must exist, or a single runner must be configured to run multiple jobs concurrently. Multiple runners must exist, or a single runner must be configured to run multiple jobs concurrently.
@ -3987,7 +3950,7 @@ This keyword must be used with `secrets:vault`.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/28321) in GitLab 13.4 and GitLab Runner 13.4. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/28321) in GitLab 13.4 and GitLab Runner 13.4.
Use `secrets:vault` to specify secrets provided by a [Hashicorp Vault](https://www.vaultproject.io/). Use `secrets:vault` to specify secrets provided by a [HashiCorp Vault](https://www.vaultproject.io/).
**Keyword type**: Job keyword. You can use it only as part of a job. **Keyword type**: Job keyword. You can use it only as part of a job.

View File

@ -92,7 +92,7 @@ end
``` ```
When this code executes, the experiment is run, a variant is assigned, and (if within a When this code executes, the experiment is run, a variant is assigned, and (if within a
controller or view) a `window.gon.experiment.pill_color` object will be available in the controller or view) a `window.gl.experiments.pill_color` object will be available in the
client layer, with details like: client layer, with details like:
- The assigned variant. - The assigned variant.
@ -522,14 +522,14 @@ shared example: [tracks assignment and records the subject](https://gitlab.com/g
This is in flux as of GitLab 13.10, and can't be documented just yet. This is in flux as of GitLab 13.10, and can't be documented just yet.
Any experiment that's been run in the request lifecycle surfaces in `window.gon.experiment`, Any experiment that's been run in the request lifecycle surfaces in and `window.gl.experiments`,
and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-0) and matches [this schema](https://gitlab.com/gitlab-org/iglu/-/blob/master/public/schemas/com.gitlab/gitlab_experiment/jsonschema/1-0-0)
so you can use it when resolving some concepts around experimentation in the client layer. so you can use it when resolving some concepts around experimentation in the client layer.
### Use experiments in Vue ### Use experiments in Vue
With the `gitlab-experiment` component, you can define slots that match the name of the With the `gitlab-experiment` component, you can define slots that match the name of the
variants pushed to `window.gon.experiment`. For example, if we alter the `pill_color` variants pushed to `window.gl.experiments`. For example, if we alter the `pill_color`
experiment to just use the default variants of `control` and `candidate` like so: experiment to just use the default variants of `control` and `candidate` like so:
```ruby ```ruby
@ -587,7 +587,51 @@ For example, the Vue component for the previously-defined `pill_color` experimen
``` ```
NOTE: NOTE:
When there is no experiment data in the `window.gon.experiment` object for the given experiment name, the `control` slot will be used, if it exists. When there is no experiment data in the `window.gl.experiments` object for the given experiment name, the `control` slot will be used, if it exists.
## Test with Jest
### Stub Helpers
You can stub experiments using the `stubExperiments` helper defined in `spec/frontend/__helpers__/experimentation_helper.js`.
```javascript
import { stubExperiments } from 'helpers/experimentation_helper';
import { getExperimentData } from '~/experimentation/utils';
describe('when my_experiment is enabled', () => {
beforeEach(() => {
stubExperiments({ my_experiment: 'candidate' });
});
it('sets the correct data', () => {
expect(getExperimentData('my_experiment')).toEqual({ experiment: 'my_experiment', variant: 'candidate' });
});
});
```
NOTE:
This method of stubbing in Jest specs will not automatically un-stub itself at the end of the test. We merge our stubbed experiment in with all the other global data in `window.gl`. If you need to remove the stubbed experiment(s) after your test or ensure a clean global object before your test, you'll need to manage the global object directly yourself:
```javascript
desribe('tests that care about global state', () => {
const originalObjects = [];
beforeEach(() => {
// For backwards compatibility for now, we're using both window.gon & window.gl
originalObjects.push(window.gon, window.gl);
});
afterEach(() => {
[window.gon, window.gl] = originalObjects;
});
it('stubs experiment in fresh global state', () => {
stubExperiment({ my_experiment: 'candidate' });
// ...
});
})
```
## Notes on feature flags ## Notes on feature flags

View File

@ -588,8 +588,6 @@ class like so:
```ruby ```ruby
class MyMigration < Gitlab::Database::Migration[1.0] class MyMigration < Gitlab::Database::Migration[1.0]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction! disable_ddl_transaction!
INDEX_NAME = 'index_name' INDEX_NAME = 'index_name'
@ -633,8 +631,6 @@ be used with a name option. For example:
```ruby ```ruby
class MyMigration < Gitlab::Database::Migration[1.0] class MyMigration < Gitlab::Database::Migration[1.0]
include Gitlab::Database::MigrationHelpers
INDEX_NAME = 'index_name' INDEX_NAME = 'index_name'
def up def up

View File

@ -208,6 +208,15 @@ Then, you can run `npm publish` either locally or by using GitLab CI/CD.
- **GitLab CI/CD:** Set an `NPM_TOKEN` [CI/CD variable](../../../ci/variables/index.md) - **GitLab CI/CD:** Set an `NPM_TOKEN` [CI/CD variable](../../../ci/variables/index.md)
under your project's **Settings > CI/CD > Variables**. under your project's **Settings > CI/CD > Variables**.
## Working with private registries
When working with private repositories, you may want to configure additional settings to ensure a secure communication channel:
```shell
# Force npm to always require authentication when accessing the registry, even for GET requests.
npm config set always-auth true
```
## Package naming convention ## Package naming convention
When you use the [instance-level endpoint](#use-the-gitlab-endpoint-for-npm-packages), only the packages with names in the format of `@scope/package-name` are available. When you use the [instance-level endpoint](#use-the-gitlab-endpoint-for-npm-packages), only the packages with names in the format of `@scope/package-name` are available.
@ -363,6 +372,10 @@ This rule has a different impact depending on the package name:
This aligns with npmjs.org's behavior. However, npmjs.org does not ever let you publish This aligns with npmjs.org's behavior. However, npmjs.org does not ever let you publish
the same version more than once, even if it has been deleted. the same version more than once, even if it has been deleted.
## `package.json` limitations
You can't publish a package if its `package.json` file exceeds 20,000 characters.
## Install a package ## Install a package
npm packages are commonly-installed by using the `npm` or `yarn` commands npm packages are commonly-installed by using the `npm` or `yarn` commands
@ -427,22 +440,29 @@ and use your organization's URL. The name is case-sensitive and must match the n
//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>" //gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken= "<your_token>"
``` ```
### npm dependencies metadata ### npm metadata
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11867) in GitLab Premium 12.6. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11867) in GitLab Premium 12.6.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3. > - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/221259) to GitLab Free in 13.3.
> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/330929) in GitLab 14.5.
In GitLab 12.6 and later, packages published to the Package Registry expose the following attributes to the npm client: The GitLab Package Registry exposes the following attributes to the npm client.
These are similar to the [abbreviated metadata format](https://github.com/npm/registry/blob/9e368cf6aaca608da5b2c378c0d53f475298b916/docs/responses/package-metadata.md#abbreviated-metadata-format):
- name - `name`
- version - `versions`
- dist-tags - `name`
- dependencies - `version`
- dependencies - `deprecated`
- devDependencies - `dependencies`
- bundleDependencies - `devDependencies`
- peerDependencies - `bundleDependencies`
- deprecated - `peerDependencies`
- `bin`
- `directories`
- `dist`
- `engines`
- `_hasShrinkwrap`
## Add npm distribution tags ## Add npm distribution tags
@ -579,6 +599,10 @@ root namespace and therefore cannot be published again using the same name.
This is also true even if the prior published package shares the same name, This is also true even if the prior published package shares the same name,
but not the version. but not the version.
#### Package JSON file is too large
Make sure that your `package.json` file does not [exceed `20,000` characters](#packagejson-limitations).
### `npm publish` returns `npm ERR! 500 Internal Server Error - PUT` ### `npm publish` returns `npm ERR! 500 Internal Server Error - PUT`
This is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/238950) in GitLab This is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/238950) in GitLab

View File

@ -4,31 +4,40 @@ group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
--- ---
# Custom issue tracker service **(FREE)** # Custom issue tracker **(FREE)**
Use a custom issue tracker that is not in the integration list. You can integrate an [external issue tracker](../../../integration/external-issue-tracker.md)
with GitLab. If your preferred issue tracker is not listed in the
[integrations list](../../../integration/external-issue-tracker.md#integration),
you can enable a custom issue tracker.
After you enable the custom issue tracker, a link to the issue tracker displays
on the left sidebar in your project.
![Custom issue tracker link](img/custom_issue_tracker_v14_5.png)
## Enable a custom issue tracker
To enable a custom issue tracker in a project: To enable a custom issue tracker in a project:
1. Go to the [Integrations page](overview.md#accessing-integrations). 1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Settings > Integrations**.
1. Select **Custom issue tracker**. 1. Select **Custom issue tracker**.
1. Select the checkbox under **Enable integration**. 1. Select the checkbox under **Enable integration**.
1. Fill in the required fields: 1. Fill in the required fields:
- **Project URL**: The URL to view all the issues in the custom issue tracker. - **Project URL**: The URL to view all the issues in the custom issue tracker.
- **Issue URL**: The URL to view an issue in the custom issue tracker. The URL must contain `:id`. - **Issue URL**: The URL to view an issue in the custom issue tracker. The URL must contain `:id`.
GitLab replaces `:id` with the issue number (for example, GitLab replaces `:id` with the issue number (for example,
`https://customissuetracker.com/project-name/:id`, which becomes `https://customissuetracker.com/project-name/123`). `https://customissuetracker.com/project-name/:id`, which becomes
`https://customissuetracker.com/project-name/123`).
- **New issue URL**: - **New issue URL**:
<!-- The line below was originally added in January 2018: https://gitlab.com/gitlab-org/gitlab/-/commit/778b231f3a5dd42ebe195d4719a26bf675093350 --> <!-- The line below was originally added in January 2018: https://gitlab.com/gitlab-org/gitlab/-/commit/778b231f3a5dd42ebe195d4719a26bf675093350 -->
**This URL is not used and removal is planned in a future release.** **This URL is not used and an [issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/327503) to remove it.**
Enter any URL here. Enter any URL.
For more information, see [issue 327503](https://gitlab.com/gitlab-org/gitlab/-/issues/327503).
1. Select **Save changes** or optionally select **Test settings**. 1. Optional. Select **Test settings**.
1. Select **Save changes**.
After you configure and enable the custom issue tracker service, a link appears on the GitLab
project pages. This link takes you to the custom issue tracker.
## Reference issues in a custom issue tracker ## Reference issues in a custom issue tracker

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -207,7 +207,7 @@ To configure a custom mailbox for Service Desk with IMAP, add the following snip
service_desk_email: service_desk_email:
enabled: true enabled: true
address: "project_contact+%{key}@example.com" address: "project_contact+%{key}@example.com"
user: "project_support@example.com" user: "project_contact@example.com"
password: "[REDACTED]" password: "[REDACTED]"
host: "imap.gmail.com" host: "imap.gmail.com"
port: 993 port: 993
@ -224,7 +224,7 @@ To configure a custom mailbox for Service Desk with IMAP, add the following snip
```ruby ```ruby
gitlab_rails['service_desk_email_enabled'] = true gitlab_rails['service_desk_email_enabled'] = true
gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com" gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com"
gitlab_rails['service_desk_email_email'] = "project_support@gmail.com" gitlab_rails['service_desk_email_email'] = "project_contact@gmail.com"
gitlab_rails['service_desk_email_password'] = "[REDACTED]" gitlab_rails['service_desk_email_password'] = "[REDACTED]"
gitlab_rails['service_desk_email_mailbox_name'] = "inbox" gitlab_rails['service_desk_email_mailbox_name'] = "inbox"
gitlab_rails['service_desk_email_idle_timeout'] = 60 gitlab_rails['service_desk_email_idle_timeout'] = 60

View File

@ -121,7 +121,9 @@ module API
not_found!('Packages') if packages.empty? not_found!('Packages') if packages.empty?
present ::Packages::Npm::PackagePresenter.new(package_name, packages), include_metadata = Feature.enabled?(:packages_npm_abbreviated_metadata, project)
present ::Packages::Npm::PackagePresenter.new(package_name, packages, include_metadata: include_metadata),
with: ::API::Entities::NpmPackage with: ::API::Entities::NpmPackage
end end
end end

View File

@ -13,7 +13,7 @@ module BulkImports
relation_hash, relation_index = data relation_hash, relation_index = data
relation_definition = import_export_config.top_relation_tree(relation) relation_definition = import_export_config.top_relation_tree(relation)
deep_transform_relation!(relation_hash, relation, relation_definition) do |key, hash| relation_object = deep_transform_relation!(relation_hash, relation, relation_definition) do |key, hash|
relation_factory.create( relation_factory.create(
relation_index: relation_index, relation_index: relation_index,
relation_sym: key.to_sym, relation_sym: key.to_sym,
@ -25,6 +25,9 @@ module BulkImports
excluded_keys: import_export_config.relation_excluded_keys(key) excluded_keys: import_export_config.relation_excluded_keys(key)
) )
end end
relation_object.assign_attributes(portable_class_sym => portable)
relation_object
end end
def load(_, object) def load(_, object)
@ -94,6 +97,10 @@ module BulkImports
def members_mapper def members_mapper
@members_mapper ||= BulkImports::UsersMapper.new(context: context) @members_mapper ||= BulkImports::UsersMapper.new(context: context)
end end
def portable_class_sym
portable.class.to_s.downcase.to_sym
end
end end
end end
end end

View File

@ -79,28 +79,6 @@ module Gitlab
increment(key, options[:scope]) > threshold_value increment(key, options[:scope]) > threshold_value
end end
# Increments the given cache key and increments the value by 1 with the
# expiration interval defined in `.rate_limits`.
#
# @param key [Symbol] Key attribute registered in `.rate_limits`
# @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
#
# @return [Integer] incremented value
def increment(key, scope)
return safe_increment(key, scope) if Feature.enabled?(:rate_limiter_safe_increment, default_enabled: :yaml)
value = 0
interval_value = interval(key)
::Gitlab::Redis::RateLimiting.with do |redis|
cache_key = action_key(key, scope)
value = redis.incr(cache_key)
redis.expire(cache_key, interval_value) if value == 1
end
value
end
# Increments a cache key that is based on the current time and interval. # Increments a cache key that is based on the current time and interval.
# So that when time passes to the next interval, the key changes and the count starts again from 0. # So that when time passes to the next interval, the key changes and the count starts again from 0.
# #
@ -110,7 +88,7 @@ module Gitlab
# @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project) # @option scope [Array<ActiveRecord>] Array of ActiveRecord models to scope throttling to a specific request (e.g. per user per project)
# #
# @return [Integer] incremented value # @return [Integer] incremented value
def safe_increment(key, scope) def increment(key, scope)
interval_value = interval(key) interval_value = interval(key)
period_key, time_elapsed_in_period = Time.now.to_i.divmod(interval_value) period_key, time_elapsed_in_period = Time.now.to_i.divmod(interval_value)

View File

@ -355,6 +355,7 @@ packages_dependency_links: :gitlab_main
packages_events: :gitlab_main packages_events: :gitlab_main
packages_helm_file_metadata: :gitlab_main packages_helm_file_metadata: :gitlab_main
packages_maven_metadata: :gitlab_main packages_maven_metadata: :gitlab_main
packages_npm_metadata: :gitlab_main
packages_nuget_dependency_link_metadata: :gitlab_main packages_nuget_dependency_link_metadata: :gitlab_main
packages_nuget_metadata: :gitlab_main packages_nuget_metadata: :gitlab_main
packages_package_file_build_infos: :gitlab_main packages_package_file_build_infos: :gitlab_main

View File

@ -31,6 +31,12 @@ module Gitlab
registered_tables.merge(tables) registered_tables.merge(tables)
end end
def sync_partitions_ignore_db_error
sync_partitions unless ENV['DISABLE_POSTGRES_PARTITION_CREATION_ON_STARTUP']
rescue ActiveRecord::ActiveRecordError, PG::Error
# ignore - happens when Rake tasks yet have to create a database, e.g. for testing
end
def sync_partitions(models_to_sync = registered_for_sync) def sync_partitions(models_to_sync = registered_for_sync)
Gitlab::AppLogger.info(message: 'Syncing dynamic postgres partitions') Gitlab::AppLogger.info(message: 'Syncing dynamic postgres partitions')

View File

@ -20,6 +20,7 @@ module Gitlab
EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000' EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'
NoRepository = Class.new(::Gitlab::Git::BaseError) NoRepository = Class.new(::Gitlab::Git::BaseError)
RepositoryExists = Class.new(::Gitlab::Git::BaseError)
InvalidRepository = Class.new(::Gitlab::Git::BaseError) InvalidRepository = Class.new(::Gitlab::Git::BaseError)
InvalidBlobName = Class.new(::Gitlab::Git::BaseError) InvalidBlobName = Class.new(::Gitlab::Git::BaseError)
InvalidRef = Class.new(::Gitlab::Git::BaseError) InvalidRef = Class.new(::Gitlab::Git::BaseError)
@ -101,6 +102,8 @@ module Gitlab
def create_repository def create_repository
wrapped_gitaly_errors do wrapped_gitaly_errors do
gitaly_repository_client.create_repository gitaly_repository_client.create_repository
rescue GRPC::AlreadyExists => e
raise RepositoryExists, e.message
end end
end end

View File

@ -48,7 +48,7 @@ module Gitlab
if gpg_key if gpg_key
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key) Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
clear_memoization(:verified_signature) clear_memoization(:gpg_signatures)
end end
yield gpg_key yield gpg_key
@ -56,16 +56,7 @@ module Gitlab
end end
def verified_signature def verified_signature
strong_memoize(:verified_signature) { gpgme_signature } gpg_signatures.first
end
def gpgme_signature
GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
# Return the first signature for now: https://gitlab.com/gitlab-org/gitlab-foss/issues/54932
break verified_signature
end
rescue GPGME::Error
nil
end end
def create_cached_signature! def create_cached_signature!
@ -77,6 +68,24 @@ module Gitlab
end end
end end
def gpg_signatures
strong_memoize(:gpg_signatures) do
signatures = []
GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
signatures << verified_signature
end
signatures
rescue GPGME::Error
[]
end
end
def multiple_signatures?
gpg_signatures.size > 1
end
def attributes(gpg_key) def attributes(gpg_key)
user_infos = user_infos(gpg_key) user_infos = user_infos(gpg_key)
verification_status = verification_status(gpg_key) verification_status = verification_status(gpg_key)
@ -93,6 +102,7 @@ module Gitlab
end end
def verification_status(gpg_key) def verification_status(gpg_key)
return :multiple_signatures if multiple_signatures? && Feature.enabled?(:multiple_gpg_signatures, @commit.project, default_enabled: :yaml)
return :unknown_key unless gpg_key return :unknown_key unless gpg_key
return :unverified_key unless gpg_key.verified? return :unverified_key unless gpg_key.verified?
return :unverified unless verified_signature&.valid? return :unverified unless verified_signature&.valid?

View File

@ -23326,6 +23326,9 @@ msgstr ""
msgid "No forks are available to you." msgid "No forks are available to you."
msgstr "" msgstr ""
msgid "No group provided"
msgstr ""
msgid "No grouping" msgid "No grouping"
msgstr "" msgstr ""
@ -23377,6 +23380,9 @@ msgstr ""
msgid "No members found" msgid "No members found"
msgstr "" msgstr ""
msgid "No memberships found"
msgstr ""
msgid "No merge requests found" msgid "No merge requests found"
msgstr "" msgstr ""
@ -24442,6 +24448,9 @@ msgstr ""
msgid "Package type must be Maven" msgid "Package type must be Maven"
msgstr "" msgstr ""
msgid "Package type must be NPM"
msgstr ""
msgid "Package type must be NuGet" msgid "Package type must be NuGet"
msgstr "" msgstr ""
@ -35112,6 +35121,9 @@ msgstr ""
msgid "This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request." msgid "This commit is part of merge request %{link_to_merge_request}. Comments created here will be created in the context of that merge request."
msgstr "" msgstr ""
msgid "This commit was signed with %{strong_open}multiple%{strong_close} signatures."
msgstr ""
msgid "This commit was signed with a %{strong_open}verified%{strong_close} signature and the committer email is verified to belong to the same user." msgid "This commit was signed with a %{strong_open}verified%{strong_close} signature and the committer email is verified to belong to the same user."
msgstr "" msgstr ""
@ -39464,6 +39476,9 @@ msgstr ""
msgid "You do not have permission to access dora metrics." msgid "You do not have permission to access dora metrics."
msgstr "" msgstr ""
msgid "You do not have permission to approve a member"
msgstr ""
msgid "You do not have permission to leave this %{namespaceType}." msgid "You do not have permission to leave this %{namespaceType}."
msgstr "" msgstr ""
@ -41767,6 +41782,9 @@ msgstr ""
msgid "starts on %{timebox_start_date}" msgid "starts on %{timebox_start_date}"
msgstr "" msgstr ""
msgid "structure is too large"
msgstr ""
msgid "stuck" msgid "stuck"
msgstr "" msgstr ""

View File

@ -19,4 +19,5 @@ Gem::Specification.new do |spec|
spec.require_paths = ['lib'] spec.require_paths = ['lib']
spec.add_runtime_dependency 'chemlab', '~> 0.9' spec.add_runtime_dependency 'chemlab', '~> 0.9'
spec.add_runtime_dependency 'zeitwerk', '~> 2.4'
end end

View File

@ -1,31 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'chemlab/library' require 'chemlab/library'
require 'zeitwerk'
loader = Zeitwerk::Loader.new
loader.push_dir(__dir__)
loader.ignore("#{__dir__}/gitlab/**/*.stub.rb") # ignore page stubs
loader.setup
# Chemlab Page Libraries for GitLab # Chemlab Page Libraries for GitLab
module Gitlab module Gitlab
include Chemlab::Library include Chemlab::Library
module Page
module Main
autoload :Login, 'gitlab/page/main/login'
autoload :SignUp, 'gitlab/page/main/sign_up'
end
module Subscriptions
autoload :New, 'gitlab/page/subscriptions/new'
end
module Admin
autoload :Dashboard, 'gitlab/page/admin/dashboard'
autoload :Subscription, 'gitlab/page/admin/subscription'
end
module Group
module Settings
autoload :Billing, 'gitlab/page/group/settings/billing'
autoload :UsageQuotas, 'gitlab/page/group/settings/usage_quotas'
end
end
end
end end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Gitlab
module Page
module Main
class Welcome < Chemlab::Page
path '/users/sign_up/welcome'
button :get_started_button
end
end
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
module Gitlab
module Page
module Main
module Welcome
# @note Defined as +button :get_started_button+
# Clicks +get_started_button+
def get_started_button
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Main::Welcome.perform do |welcome|
# expect(welcome.get_started_button_element).to exist
# end
# @return [Watir::Button] The raw +Button+ element
def get_started_button_element
# This is a stub, used for indexing. The method is dynamically generated.
end
# @example
# Gitlab::Page::Main::Welcome.perform do |welcome|
# expect(welcome).to be_get_started_button
# end
# @return [Boolean] true if the +get_started_button+ element is present on the page
def get_started_button?
# This is a stub, used for indexing. The method is dynamically generated.
end
end
end
end
end

View File

@ -62,7 +62,7 @@ RSpec.describe Dashboard::TodosController do
create(:issue, project: project, assignees: [user]) create(:issue, project: project, assignees: [user])
group_2 = create(:group) group_2 = create(:group)
group_2.add_owner(user) group_2.add_owner(user)
project_2 = create(:project) project_2 = create(:project, namespace: user.namespace)
project_2.add_developer(user) project_2.add_developer(user)
merge_request_2 = create(:merge_request, source_project: project_2) merge_request_2 = create(:merge_request, source_project: project_2)
create(:todo, project: project, author: author, user: user, target: merge_request_2) create(:todo, project: project, author: author, user: user, target: merge_request_2)

View File

@ -86,10 +86,11 @@ RSpec.describe Projects::MergeRequests::DiffsController do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:maintainer) { true }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
before do before do
project.add_maintainer(user) project.add_maintainer(user) if maintainer
sign_in(user) sign_in(user)
end end
@ -383,8 +384,9 @@ RSpec.describe Projects::MergeRequests::DiffsController do
end end
context 'when the user cannot view the merge request' do context 'when the user cannot view the merge request' do
let(:maintainer) { false }
before do before do
project.team.truncate
diff_for_path(old_path: existing_path, new_path: existing_path) diff_for_path(old_path: existing_path, new_path: existing_path)
end end

View File

@ -10,7 +10,8 @@ RSpec.describe Projects::MergeRequestsController do
let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) } let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) }
let(:user) { project.owner } let(:user) { project.owner }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: merge_request_source_project, allow_maintainer_to_push: false) }
let(:merge_request_source_project) { project }
before do before do
sign_in(user) sign_in(user)
@ -2073,8 +2074,6 @@ RSpec.describe Projects::MergeRequestsController do
end end
describe 'POST #rebase' do describe 'POST #rebase' do
let(:viewer) { user }
def post_rebase def post_rebase
post :rebase, params: { namespace_id: project.namespace, project_id: project, id: merge_request } post :rebase, params: { namespace_id: project.namespace, project_id: project, id: merge_request }
end end
@ -2085,7 +2084,7 @@ RSpec.describe Projects::MergeRequestsController do
context 'successfully' do context 'successfully' do
it 'enqeues a RebaseWorker' do it 'enqeues a RebaseWorker' do
expect_rebase_worker_for(viewer) expect_rebase_worker_for(user)
post_rebase post_rebase
@ -2108,17 +2107,17 @@ RSpec.describe Projects::MergeRequestsController do
context 'with a forked project' do context 'with a forked project' do
let(:forked_project) { fork_project(project, fork_owner, repository: true) } let(:forked_project) { fork_project(project, fork_owner, repository: true) }
let(:fork_owner) { create(:user) } let(:fork_owner) { create(:user) }
let(:merge_request_source_project) { forked_project }
before do
project.add_developer(fork_owner)
merge_request.update!(source_project: forked_project)
forked_project.add_reporter(user)
end
context 'user cannot push to source branch' do context 'user cannot push to source branch' do
before do
project.add_developer(fork_owner)
forked_project.add_reporter(user)
end
it 'returns 404' do it 'returns 404' do
expect_rebase_worker_for(viewer).never expect_rebase_worker_for(user).never
post_rebase post_rebase

View File

@ -499,13 +499,12 @@ RSpec.describe RegistrationsController do
expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}") expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}")
end end
it 'sets the username and caller_id in the context' do it 'sets the caller_id in the context' do
expect(controller).to receive(:create).and_wrap_original do |m, *args| expect(controller).to receive(:create).and_wrap_original do |m, *args|
m.call(*args) m.call(*args)
expect(Gitlab::ApplicationContext.current) expect(Gitlab::ApplicationContext.current)
.to include('meta.user' => base_user_params[:username], .to include('meta.caller_id' => 'RegistrationsController#create')
'meta.caller_id' => 'RegistrationsController#create')
end end
subject subject

View File

@ -2,7 +2,7 @@
FactoryBot.define do FactoryBot.define do
factory :project_namespace, class: 'Namespaces::ProjectNamespace' do factory :project_namespace, class: 'Namespaces::ProjectNamespace' do
project association :project, factory: :project, strategy: :build
parent { project.namespace } parent { project.namespace }
visibility_level { project.visibility_level } visibility_level { project.visibility_level }
name { project.name } name { project.name }

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
FactoryBot.define do
factory :npm_metadatum, class: 'Packages::Npm::Metadatum' do
package { association(:npm_package) }
package_json do
{
'name': package.name,
'version': package.version,
'dist': {
'tarball': 'http://localhost/tarball.tgz',
'shasum': '1234567890'
}
}
end
end
end

View File

@ -115,6 +115,19 @@ RSpec.describe 'GPG signed commits' do
end end
end end
it 'unverified signature: commit contains multiple GPG signatures' do
user_1_key
visit project_commit_path(project, GpgHelpers::MULTIPLE_SIGNATURES_SHA)
wait_for_all_requests
page.find('.gpg-status-box', text: 'Unverified').click
within '.popover' do
expect(page).to have_content "This commit was signed with multiple signatures."
end
end
it 'verified and the gpg user has a gitlab profile' do it 'verified and the gpg user has a gitlab profile' do
user_1_key user_1_key
@ -169,7 +182,7 @@ RSpec.describe 'GPG signed commits' do
page.find('.gpg-status-box', text: 'Unverified').click page.find('.gpg-status-box', text: 'Unverified').click
within '.popover' do within '.popover' do
expect(page).to have_content 'This commit was signed with an unverified signature' expect(page).to have_content 'This commit was signed with multiple signatures.'
end end
end end
end end

View File

@ -36,11 +36,11 @@
".{1,}": { "type": "string" } ".{1,}": { "type": "string" }
} }
}, },
"deprecated": { "deprecated": { "type": "string"},
"type": "object", "bin": { "type": "string" },
"patternProperties": { "directories": { "type": "array" },
".{1,}": { "type": "string" } "engines": { "type": "object" },
} "_hasShrinkwrap": { "type": "boolean" },
} "additionalProperties": true
} }
} }

View File

@ -14,7 +14,8 @@
"express":"^4.16.4" "express":"^4.16.4"
}, },
"dist":{ "dist":{
"shasum":"f572d396fae9206628714fb2ce00f72e94f2258f" "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f",
"tarball":"http://localhost/npm/package.tgz"
} }
} }
}, },

View File

@ -28,7 +28,8 @@
"express":"^4.16.4" "express":"^4.16.4"
}, },
"dist":{ "dist":{
"shasum":"f572d396fae9206628714fb2ce00f72e94f2258f" "shasum":"f572d396fae9206628714fb2ce00f72e94f2258f",
"tarball":"http://localhost/npm/package.tgz"
} }
} }
}, },

View File

@ -1,5 +1,6 @@
import { merge } from 'lodash'; import { merge } from 'lodash';
// This helper is for specs that use `gitlab/experimentation` module
export function withGonExperiment(experimentKey, value = true) { export function withGonExperiment(experimentKey, value = true) {
let origGon; let origGon;
@ -12,16 +13,26 @@ export function withGonExperiment(experimentKey, value = true) {
window.gon = origGon; window.gon = origGon;
}); });
} }
// This helper is for specs that use `gitlab-experiment` utilities, which have a different schema that gets pushed via Gon compared to `Experimentation Module`
export function assignGitlabExperiment(experimentKey, variant) {
let origGon;
beforeEach(() => { // The following helper is for specs that use `gitlab-experiment` utilities,
origGon = window.gon; // which have a different schema that gets pushed to the frontend compared to
window.gon = { experiment: { [experimentKey]: { variant } } }; // the `Experimentation` Module.
}); //
// Usage: stubExperiments({ experiment_feature_flag_name: 'variant_name', ... })
export function stubExperiments(experiments = {}) {
// Deprecated
window.gon = window.gon || {};
window.gon.experiment = window.gon.experiment || {};
// Preferred
window.gl = window.gl || {};
window.gl.experiments = window.gl.experiemnts || {};
afterEach(() => { Object.entries(experiments).forEach(([name, variant]) => {
window.gon = origGon; const experimentData = { experiment: name, variant };
// Deprecated
window.gon.experiment[name] = experimentData;
// Preferred
window.gl.experiments[name] = experimentData;
}); });
} }

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import NewBoardButton from '~/boards/components/new_board_button.vue'; import NewBoardButton from '~/boards/components/new_board_button.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { assignGitlabExperiment } from 'helpers/experimentation_helper'; import { stubExperiments } from 'helpers/experimentation_helper';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
const FEATURE = 'prominent_create_board_btn'; const FEATURE = 'prominent_create_board_btn';
@ -28,7 +28,9 @@ describe('NewBoardButton', () => {
}); });
describe('control variant', () => { describe('control variant', () => {
assignGitlabExperiment(FEATURE, 'control'); beforeAll(() => {
stubExperiments({ [FEATURE]: 'control' });
});
it('renders nothing', () => { it('renders nothing', () => {
wrapper = createComponent(); wrapper = createComponent();
@ -38,7 +40,9 @@ describe('NewBoardButton', () => {
}); });
describe('candidate variant', () => { describe('candidate variant', () => {
assignGitlabExperiment(FEATURE, 'candidate'); beforeAll(() => {
stubExperiments({ [FEATURE]: 'candidate' });
});
it('renders New board button when `candidate` variant', () => { it('renders New board button when `candidate` variant', () => {
wrapper = createComponent(); wrapper = createComponent();

View File

@ -241,18 +241,19 @@ describe('DiffFileHeader component', () => {
}); });
describe('for any file', () => { describe('for any file', () => {
const otherModes = Object.keys(diffViewerModes).filter((m) => m !== 'mode_changed'); const allModes = Object.keys(diffViewerModes).map((m) => [m]);
it('for mode_changed file mode displays mode changes', () => { it.each(allModes)('for %s file mode displays mode changes', (mode) => {
createComponent({ createComponent({
props: { props: {
diffFile: { diffFile: {
...diffFile, ...diffFile,
mode_changed: true,
a_mode: 'old-mode', a_mode: 'old-mode',
b_mode: 'new-mode', b_mode: 'new-mode',
viewer: { viewer: {
...diffFile.viewer, ...diffFile.viewer,
name: diffViewerModes.mode_changed, name: diffViewerModes[mode],
}, },
}, },
}, },
@ -260,13 +261,14 @@ describe('DiffFileHeader component', () => {
expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/); expect(findModeChangedLine().text()).toMatch(/old-mode.+new-mode/);
}); });
it.each(otherModes.map((m) => [m]))( it.each(allModes.filter((m) => m[0] !== 'mode_changed'))(
'for %s file mode does not display mode changes', 'for %s file mode does not display mode changes',
(mode) => { (mode) => {
createComponent({ createComponent({
props: { props: {
diffFile: { diffFile: {
...diffFile, ...diffFile,
mode_changed: false,
a_mode: 'old-mode', a_mode: 'old-mode',
b_mode: 'new-mode', b_mode: 'new-mode',
viewer: { viewer: {

View File

@ -1,4 +1,4 @@
import { assignGitlabExperiment } from 'helpers/experimentation_helper'; import { stubExperiments } from 'helpers/experimentation_helper';
import { import {
DEFAULT_VARIANT, DEFAULT_VARIANT,
CANDIDATE_VARIANT, CANDIDATE_VARIANT,
@ -7,15 +7,45 @@ import {
import * as experimentUtils from '~/experimentation/utils'; import * as experimentUtils from '~/experimentation/utils';
describe('experiment Utilities', () => { describe('experiment Utilities', () => {
const TEST_KEY = 'abc'; const ABC_KEY = 'abc';
const DEF_KEY = 'def';
let origGon;
let origGl;
beforeEach(() => {
origGon = window.gon;
origGl = window.gl;
window.gon.experiment = {};
window.gl.experiments = {};
});
afterEach(() => {
window.gon = origGon;
window.gl = origGl;
});
describe('getExperimentData', () => { describe('getExperimentData', () => {
const ABC_DATA = '_abc_data_';
const ABC_DATA2 = '_updated_abc_data_';
const DEF_DATA = '_def_data_';
describe.each` describe.each`
gon | input | output gonData | glData | input | output
${[TEST_KEY, '_data_']} | ${[TEST_KEY]} | ${{ variant: '_data_' }} ${[ABC_KEY, ABC_DATA]} | ${[]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
${[]} | ${[TEST_KEY]} | ${undefined} ${[]} | ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
`('with input=$input and gon=$gon', ({ gon, input, output }) => { ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
assignGitlabExperiment(...gon); ${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[DEF_KEY]} | ${{ experiment: DEF_KEY, variant: DEF_DATA }}
${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY, ABC_DATA2]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA2 }}
${[]} | ${[]} | ${[ABC_KEY]} | ${undefined}
`('with input=$input, gon=$gonData, & gl=$glData', ({ gonData, glData, input, output }) => {
beforeEach(() => {
const [gonKey, gonVariant] = gonData;
const [glKey, glVariant] = glData;
if (gonKey) window.gon.experiment[gonKey] = { experiment: gonKey, variant: gonVariant };
if (glKey) window.gl.experiments[glKey] = { experiment: glKey, variant: glVariant };
});
it(`returns ${output}`, () => { it(`returns ${output}`, () => {
expect(experimentUtils.getExperimentData(...input)).toEqual(output); expect(experimentUtils.getExperimentData(...input)).toEqual(output);
@ -25,66 +55,47 @@ describe('experiment Utilities', () => {
describe('getAllExperimentContexts', () => { describe('getAllExperimentContexts', () => {
const schema = TRACKING_CONTEXT_SCHEMA; const schema = TRACKING_CONTEXT_SCHEMA;
let origGon;
beforeEach(() => {
origGon = window.gon;
});
afterEach(() => {
window.gon = origGon;
});
it('collects all of the experiment contexts into a single array', () => { it('collects all of the experiment contexts into a single array', () => {
const experiments = [ const experiments = { [ABC_KEY]: 'candidate', [DEF_KEY]: 'control', ghi: 'blue' };
{ experiment: 'abc', variant: 'candidate' },
{ experiment: 'def', variant: 'control' }, stubExperiments(experiments);
{ experiment: 'ghi', variant: 'blue' },
];
window.gon = {
experiment: experiments.reduce((collector, { experiment, variant }) => {
return { ...collector, [experiment]: { experiment, variant } };
}, {}),
};
expect(experimentUtils.getAllExperimentContexts()).toEqual( expect(experimentUtils.getAllExperimentContexts()).toEqual(
experiments.map((data) => ({ schema, data })), Object.entries(experiments).map(([experiment, variant]) => ({
schema,
data: { experiment, variant },
})),
); );
}); });
it('returns an empty array if there are no experiments', () => { it('returns an empty array if there are no experiments', () => {
window.gon.experiment = {};
expect(experimentUtils.getAllExperimentContexts()).toEqual([]); expect(experimentUtils.getAllExperimentContexts()).toEqual([]);
}); });
it('includes all additional experiment data', () => {
const experiment = 'experimentWithCustomData';
const data = { experiment, variant: 'control', color: 'blue', style: 'rounded' };
window.gon.experiment[experiment] = data;
expect(experimentUtils.getAllExperimentContexts()).toContainEqual({ schema, data });
});
}); });
describe('isExperimentVariant', () => { describe('isExperimentVariant', () => {
describe.each` describe.each`
gon | input | output experiment | variant | input | output
${[TEST_KEY, DEFAULT_VARIANT]} | ${[TEST_KEY, DEFAULT_VARIANT]} | ${true} ${ABC_KEY} | ${DEFAULT_VARIANT} | ${[ABC_KEY, DEFAULT_VARIANT]} | ${true}
${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_variant_name']} | ${true} ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_variant_name']} | ${true}
${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_bogus_name']} | ${false} ${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_bogus_name']} | ${false}
${[TEST_KEY, '_variant_name']} | ${['boguskey', '_variant_name']} | ${false} ${ABC_KEY} | ${'_variant_name'} | ${['boguskey', '_variant_name']} | ${false}
${[]} | ${[TEST_KEY, '_variant_name']} | ${false} ${undefined} | ${undefined} | ${[ABC_KEY, '_variant_name']} | ${false}
`('with input=$input and gon=$gon', ({ gon, input, output }) => { `(
assignGitlabExperiment(...gon); 'with input=$input, experiment=$experiment, variant=$variant',
({ experiment, variant, input, output }) => {
it(`returns ${output}`, () => {
if (experiment) stubExperiments({ [experiment]: variant });
it(`returns ${output}`, () => { expect(experimentUtils.isExperimentVariant(...input)).toEqual(output);
expect(experimentUtils.isExperimentVariant(...input)).toEqual(output); });
}); },
}); );
}); });
describe('experiment', () => { describe('experiment', () => {
const experiment = 'marley';
const useSpy = jest.fn(); const useSpy = jest.fn();
const controlSpy = jest.fn(); const controlSpy = jest.fn();
const trySpy = jest.fn(); const trySpy = jest.fn();
@ -98,49 +109,62 @@ describe('experiment Utilities', () => {
}; };
describe('when there is no experiment data', () => { describe('when there is no experiment data', () => {
it('calls control variant', () => { it('calls the use variant', () => {
experimentUtils.experiment('marley', variants); experimentUtils.experiment(experiment, variants);
expect(useSpy).toHaveBeenCalled();
});
});
describe('when experiment variant is "control"', () => {
assignGitlabExperiment('marley', DEFAULT_VARIANT);
it('calls the control variant', () => {
experimentUtils.experiment('marley', variants);
expect(useSpy).toHaveBeenCalled(); expect(useSpy).toHaveBeenCalled();
}); });
describe("when 'control' is provided instead of 'use'", () => { describe("when 'control' is provided instead of 'use'", () => {
it('calls the control variant', () => { it('calls the control variant', () => {
experimentUtils.experiment('marley', { control: controlSpy }); experimentUtils.experiment(experiment, { control: controlSpy });
expect(controlSpy).toHaveBeenCalled();
});
});
});
describe('when experiment variant is "control"', () => {
beforeEach(() => {
stubExperiments({ [experiment]: DEFAULT_VARIANT });
});
it('calls the use variant', () => {
experimentUtils.experiment(experiment, variants);
expect(useSpy).toHaveBeenCalled();
});
describe("when 'control' is provided instead of 'use'", () => {
it('calls the control variant', () => {
experimentUtils.experiment(experiment, { control: controlSpy });
expect(controlSpy).toHaveBeenCalled(); expect(controlSpy).toHaveBeenCalled();
}); });
}); });
}); });
describe('when experiment variant is "candidate"', () => { describe('when experiment variant is "candidate"', () => {
assignGitlabExperiment('marley', CANDIDATE_VARIANT); beforeEach(() => {
stubExperiments({ [experiment]: CANDIDATE_VARIANT });
});
it('calls the candidate variant', () => { it('calls the try variant', () => {
experimentUtils.experiment('marley', variants); experimentUtils.experiment(experiment, variants);
expect(trySpy).toHaveBeenCalled(); expect(trySpy).toHaveBeenCalled();
}); });
describe("when 'candidate' is provided instead of 'try'", () => { describe("when 'candidate' is provided instead of 'try'", () => {
it('calls the control variant', () => { it('calls the candidate variant', () => {
experimentUtils.experiment('marley', { candidate: candidateSpy }); experimentUtils.experiment(experiment, { candidate: candidateSpy });
expect(candidateSpy).toHaveBeenCalled(); expect(candidateSpy).toHaveBeenCalled();
}); });
}); });
}); });
describe('when experiment variant is "get_up_stand_up"', () => { describe('when experiment variant is "get_up_stand_up"', () => {
assignGitlabExperiment('marley', 'get_up_stand_up'); beforeEach(() => {
stubExperiments({ [experiment]: 'get_up_stand_up' });
});
it('calls the get-up-stand-up variant', () => { it('calls the get-up-stand-up variant', () => {
experimentUtils.experiment('marley', variants); experimentUtils.experiment(experiment, variants);
expect(getUpStandUpSpy).toHaveBeenCalled(); expect(getUpStandUpSpy).toHaveBeenCalled();
}); });
}); });
@ -148,14 +172,17 @@ describe('experiment Utilities', () => {
describe('getExperimentVariant', () => { describe('getExperimentVariant', () => {
it.each` it.each`
gon | input | output experiment | variant | input | output
${{ experiment: { [TEST_KEY]: { variant: DEFAULT_VARIANT } } }} | ${[TEST_KEY]} | ${DEFAULT_VARIANT} ${ABC_KEY} | ${DEFAULT_VARIANT} | ${ABC_KEY} | ${DEFAULT_VARIANT}
${{ experiment: { [TEST_KEY]: { variant: CANDIDATE_VARIANT } } }} | ${[TEST_KEY]} | ${CANDIDATE_VARIANT} ${ABC_KEY} | ${CANDIDATE_VARIANT} | ${ABC_KEY} | ${CANDIDATE_VARIANT}
${{}} | ${[TEST_KEY]} | ${DEFAULT_VARIANT} ${undefined} | ${undefined} | ${ABC_KEY} | ${DEFAULT_VARIANT}
`('with input=$input and gon=$gon, returns $output', ({ gon, input, output }) => { `(
window.gon = gon; 'with input=$input, experiment=$experiment, & variant=$variant; returns $output',
({ experiment, variant, input, output }) => {
stubExperiments({ [experiment]: variant });
expect(experimentUtils.getExperimentVariant(...input)).toEqual(output); expect(experimentUtils.getExperimentVariant(input)).toEqual(output);
}); },
);
}); });
}); });

View File

@ -1,12 +1,14 @@
import { GlAlert, GlLink } from '@gitlab/ui'; import { GlAlert, GlLink, GlEmptyState } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue'; import JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue'; import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_button.vue';
import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue'; import SignInButton from '~/jira_connect/subscriptions/components/sign_in_button.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store'; import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils', () => ({ jest.mock('~/jira_connect/subscriptions/utils', () => ({
retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }), retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }),
@ -20,6 +22,8 @@ describe('JiraConnectApp', () => {
const findAlertLink = () => findAlert().findComponent(GlLink); const findAlertLink = () => findAlert().findComponent(GlLink);
const findSignInButton = () => wrapper.findComponent(SignInButton); const findSignInButton = () => wrapper.findComponent(SignInButton);
const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton); const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const createComponent = ({ provide, mountFn = shallowMount } = {}) => { const createComponent = ({ provide, mountFn = shallowMount } = {}) => {
store = createStore(); store = createStore();
@ -36,91 +40,114 @@ describe('JiraConnectApp', () => {
describe('template', () => { describe('template', () => {
describe.each` describe.each`
scenario | usersPath | expectSignInButton | expectNamespaceButton scenario | usersPath | subscriptions | expectSignInButton | expectEmptyState | expectNamespaceButton | expectSubscriptionsList
${'user is not signed in'} | ${'/users'} | ${true} | ${false} ${'user is not signed in with subscriptions'} | ${'/users'} | ${[mockSubscription]} | ${true} | ${false} | ${false} | ${true}
${'user is signed in'} | ${undefined} | ${false} | ${true} ${'user is not signed in without subscriptions'} | ${'/users'} | ${undefined} | ${true} | ${false} | ${false} | ${false}
`('when $scenario', ({ usersPath, expectSignInButton, expectNamespaceButton }) => { ${'user is signed in with subscriptions'} | ${undefined} | ${[mockSubscription]} | ${false} | ${false} | ${true} | ${true}
beforeEach(() => { ${'user is signed in without subscriptions'} | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false}
createComponent({ `(
provide: { 'when $scenario',
usersPath, ({
}, usersPath,
expectSignInButton,
subscriptions,
expectEmptyState,
expectNamespaceButton,
expectSubscriptionsList,
}) => {
beforeEach(() => {
createComponent({
provide: {
usersPath,
subscriptions,
},
});
}); });
});
it('renders sign in button as expected', () => { it(`${expectSignInButton ? 'renders' : 'does not render'} sign in button`, () => {
expect(findSignInButton().exists()).toBe(expectSignInButton); expect(findSignInButton().exists()).toBe(expectSignInButton);
}); });
it('renders "Add Namespace" button as expected', () => { it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton); expect(findEmptyState().exists()).toBe(expectEmptyState);
}); });
});
describe('alert', () => { it(`${
it.each` expectNamespaceButton ? 'renders' : 'does not render'
message | variant | alertShouldRender } button to add namespace`, () => {
${'Test error'} | ${'danger'} | ${true} expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton);
${'Test notice'} | ${'info'} | ${true} });
${''} | ${undefined} | ${false}
${undefined} | ${undefined} | ${false}
`(
'renders correct alert when message is `$message` and variant is `$variant`',
async ({ message, alertShouldRender, variant }) => {
createComponent();
store.commit(SET_ALERT, { message, variant }); it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
await wrapper.vm.$nextTick(); expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
});
},
);
});
const alert = findAlert(); describe('alert', () => {
it.each`
expect(alert.exists()).toBe(alertShouldRender); message | variant | alertShouldRender
if (alertShouldRender) { ${'Test error'} | ${'danger'} | ${true}
expect(alert.isVisible()).toBe(alertShouldRender); ${'Test notice'} | ${'info'} | ${true}
expect(alert.html()).toContain(message); ${''} | ${undefined} | ${false}
expect(alert.props('variant')).toBe(variant); ${undefined} | ${undefined} | ${false}
expect(findAlertLink().exists()).toBe(false); `(
} 'renders correct alert when message is `$message` and variant is `$variant`',
}, async ({ message, alertShouldRender, variant }) => {
);
it('hides alert on @dismiss event', async () => {
createComponent(); createComponent();
store.commit(SET_ALERT, { message: 'test message' }); store.commit(SET_ALERT, { message, variant });
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
findAlert().vm.$emit('dismiss'); const alert = findAlert();
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false); expect(alert.exists()).toBe(alertShouldRender);
if (alertShouldRender) {
expect(alert.isVisible()).toBe(alertShouldRender);
expect(alert.html()).toContain(message);
expect(alert.props('variant')).toBe(variant);
expect(findAlertLink().exists()).toBe(false);
}
},
);
it('hides alert on @dismiss event', async () => {
createComponent();
store.commit(SET_ALERT, { message: 'test message' });
await wrapper.vm.$nextTick();
findAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(findAlert().exists()).toBe(false);
});
it('renders link when `linkUrl` is set', async () => {
createComponent({ mountFn: mount });
store.commit(SET_ALERT, {
message: __('test message %{linkStart}test link%{linkEnd}'),
linkUrl: 'https://gitlab.com',
}); });
await wrapper.vm.$nextTick();
it('renders link when `linkUrl` is set', async () => { const alertLink = findAlertLink();
createComponent({ mountFn: mount });
store.commit(SET_ALERT, { expect(alertLink.exists()).toBe(true);
message: __('test message %{linkStart}test link%{linkEnd}'), expect(alertLink.text()).toContain('test link');
linkUrl: 'https://gitlab.com', expect(alertLink.attributes('href')).toBe('https://gitlab.com');
}); });
await wrapper.vm.$nextTick();
const alertLink = findAlertLink(); describe('when alert is set in localStoage', () => {
it('renders alert on mount', () => {
createComponent();
expect(alertLink.exists()).toBe(true); const alert = findAlert();
expect(alertLink.text()).toContain('test link');
expect(alertLink.attributes('href')).toBe('https://gitlab.com');
});
describe('when alert is set in localStoage', () => { expect(alert.exists()).toBe(true);
it('renders alert on mount', () => { expect(alert.html()).toContain('error message');
createComponent();
const alert = findAlert();
expect(alert.exists()).toBe(true);
expect(alert.html()).toContain('error message');
});
}); });
}); });
}); });

View File

@ -1,12 +1,15 @@
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import * as JiraConnectApi from '~/jira_connect/subscriptions/api'; import * as JiraConnectApi from '~/jira_connect/subscriptions/api';
import GroupItemName from '~/jira_connect/subscriptions/components/group_item_name.vue';
import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue'; import SubscriptionsList from '~/jira_connect/subscriptions/components/subscriptions_list.vue';
import createStore from '~/jira_connect/subscriptions/store'; import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { reloadPage } from '~/jira_connect/subscriptions/utils'; import { reloadPage } from '~/jira_connect/subscriptions/utils';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockSubscription } from '../mock_data'; import { mockSubscription } from '../mock_data';
jest.mock('~/jira_connect/subscriptions/utils'); jest.mock('~/jira_connect/subscriptions/utils');
@ -15,11 +18,13 @@ describe('SubscriptionsList', () => {
let wrapper; let wrapper;
let store; let store;
const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => { const createComponent = () => {
store = createStore(); store = createStore();
wrapper = mountFn(SubscriptionsList, { wrapper = mount(SubscriptionsList, {
provide, provide: {
subscriptions: [mockSubscription],
},
store, store,
}); });
}; };
@ -28,28 +33,28 @@ describe('SubscriptionsList', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); const findUnlinkButton = () => wrapper.findComponent(GlButton);
const findGlTable = () => wrapper.findComponent(GlTable);
const findUnlinkButton = () => findGlTable().findComponent(GlButton);
const clickUnlinkButton = () => findUnlinkButton().trigger('click'); const clickUnlinkButton = () => findUnlinkButton().trigger('click');
describe('template', () => { describe('template', () => {
it('renders GlEmptyState when subscriptions is empty', () => { beforeEach(() => {
createComponent(); createComponent();
expect(findGlEmptyState().exists()).toBe(true);
expect(findGlTable().exists()).toBe(false);
}); });
it('renders GlTable when subscriptions are present', () => { it('renders "name" cell correctly', () => {
createComponent({ const groupItemNames = wrapper.findAllComponents(GroupItemName);
provide: { expect(groupItemNames.wrappers).toHaveLength(1);
subscriptions: [mockSubscription],
},
});
expect(findGlEmptyState().exists()).toBe(false); const item = groupItemNames.at(0);
expect(findGlTable().exists()).toBe(true); expect(item.props('group')).toBe(mockSubscription.group);
});
it('renders "created at" cell correctly', () => {
const timeAgoTooltips = wrapper.findAllComponents(TimeagoTooltip);
expect(timeAgoTooltips.wrappers).toHaveLength(1);
const item = timeAgoTooltips.at(0);
expect(item.props('time')).toBe(mockSubscription.created_at);
}); });
}); });
@ -57,12 +62,7 @@ describe('SubscriptionsList', () => {
let removeSubscriptionSpy; let removeSubscriptionSpy;
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent();
mountFn: mount,
provide: {
subscriptions: [mockSubscription],
},
});
removeSubscriptionSpy = jest.spyOn(JiraConnectApi, 'removeSubscription').mockResolvedValue(); removeSubscriptionSpy = jest.spyOn(JiraConnectApi, 'removeSubscription').mockResolvedValue();
}); });

View File

@ -46,7 +46,7 @@ describe('Wip', () => {
is_new_mr_data: true, is_new_mr_data: true,
}; };
describe('handleRemoveWIP', () => { describe('handleRemoveDraft', () => {
it('should make a request to service and handle response', (done) => { it('should make a request to service and handle response', (done) => {
const vm = createComponent(); const vm = createComponent();
@ -59,7 +59,7 @@ describe('Wip', () => {
}), }),
); );
vm.handleRemoveWIP(); vm.handleRemoveDraft();
setImmediate(() => { setImmediate(() => {
expect(vm.isMakingRequest).toBeTruthy(); expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
@ -84,7 +84,7 @@ describe('Wip', () => {
expect(el.innerText).toContain('This merge request is still a draft.'); expect(el.innerText).toContain('This merge request is still a draft.');
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(el.querySelector('button').innerText).toContain('Merge'); expect(el.querySelector('button').innerText).toContain('Merge');
expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain( expect(el.querySelector('.js-remove-draft').innerText.replace(/\s\s+/g, ' ')).toContain(
'Mark as ready', 'Mark as ready',
); );
}); });
@ -93,7 +93,7 @@ describe('Wip', () => {
vm.mr.removeWIPPath = ''; vm.mr.removeWIPPath = '';
Vue.nextTick(() => { Vue.nextTick(() => {
expect(el.querySelector('.js-remove-wip')).toEqual(null); expect(el.querySelector('.js-remove-draft')).toEqual(null);
done(); done();
}); });
}); });

View File

@ -15,7 +15,7 @@ describe('getStateKey', () => {
branchMissing: false, branchMissing: false,
commitsCount: 2, commitsCount: 2,
hasConflicts: false, hasConflicts: false,
workInProgress: false, draft: false,
}; };
const bound = getStateKey.bind(context); const bound = getStateKey.bind(context);
@ -49,9 +49,9 @@ describe('getStateKey', () => {
expect(bound()).toEqual('unresolvedDiscussions'); expect(bound()).toEqual('unresolvedDiscussions');
context.workInProgress = true; context.draft = true;
expect(bound()).toEqual('workInProgress'); expect(bound()).toEqual('draft');
context.onlyAllowMergeIfPipelineSucceeds = true; context.onlyAllowMergeIfPipelineSucceeds = true;
context.isPipelineFailed = true; context.isPipelineFailed = true;
@ -99,7 +99,7 @@ describe('getStateKey', () => {
branchMissing: false, branchMissing: false,
commitsCount: 2, commitsCount: 2,
hasConflicts: false, hasConflicts: false,
workInProgress: false, draft: false,
}; };
const bound = getStateKey.bind(context); const bound = getStateKey.bind(context);

View File

@ -1,55 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::MergeRequests::SetWip do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
describe '#resolve' do
let(:wip) { true }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, wip: wip) }
it_behaves_like 'permission level for merge request mutation is correctly verified'
context 'when the user can update the merge request' do
before do
merge_request.project.add_developer(user)
end
it 'returns the merge request as a wip' do
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request).to be_work_in_progress
expect(subject[:errors]).to be_empty
end
it 'returns errors merge request could not be updated' do
# Make the merge request invalid
merge_request.allow_broken = true
merge_request.update!(source_project: nil)
expect(subject[:errors]).not_to be_empty
end
context 'when passing wip as false' do
let(:wip) { false }
it 'removes `wip` from the title' do
merge_request.update!(title: "WIP: working on it")
expect(mutated_merge_request).not_to be_work_in_progress
end
it 'does not do anything if the title did not start with wip' do
expect(mutated_merge_request).not_to be_work_in_progress
end
end
end
end
end

View File

@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
notes discussions user_permissions id iid title title_html description notes discussions user_permissions id iid title title_html description
description_html state created_at updated_at source_project target_project description_html state created_at updated_at source_project target_project
project project_id source_project_id target_project_id source_branch project project_id source_project_id target_project_id source_branch
target_branch work_in_progress draft merge_when_pipeline_succeeds diff_head_sha target_branch draft merge_when_pipeline_succeeds diff_head_sha
merge_commit_sha user_notes_count user_discussions_count should_remove_source_branch merge_commit_sha user_notes_count user_discussions_count should_remove_source_branch
diff_refs diff_stats diff_stats_summary diff_refs diff_stats diff_stats_summary
force_remove_source_branch force_remove_source_branch

View File

@ -3,14 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Types::MutationType do RSpec.describe Types::MutationType do
it 'is expected to have the deprecated MergeRequestSetWip' do
field = get_field('MergeRequestSetWip')
expect(field).to be_present
expect(field.deprecation_reason).to be_present
expect(field.resolver).to eq(Mutations::MergeRequests::SetWip)
end
it 'is expected to have the MergeRequestSetDraft' do it 'is expected to have the MergeRequestSetDraft' do
expect(described_class).to have_graphql_mutation(Mutations::MergeRequests::SetDraft) expect(described_class).to have_graphql_mutation(Mutations::MergeRequests::SetDraft)
end end

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