Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
26881dd926
commit
6fd750c192
|
@ -105,5 +105,16 @@ In this rollout issue, ensure the scoped `experiment::` label is kept accurate.
|
|||
/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"
|
||||
/milestone %"Next 1-3 releases"
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
import { escape } from 'lodash';
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { IdState } from 'vendor/vue-virtual-scroller';
|
||||
import { diffViewerModes } from '~/ide/constants';
|
||||
import { scrollToElement } from '~/lib/utils/common_utils';
|
||||
import { truncateSha } from '~/lib/utils/text_utility';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
|
@ -181,7 +180,7 @@ export default {
|
|||
return this.diffFile.renamed_file;
|
||||
},
|
||||
isModeChanged() {
|
||||
return this.diffFile.viewer.name === diffViewerModes.mode_changed;
|
||||
return this.diffFile.mode_changed;
|
||||
},
|
||||
expandDiffToFullFileTitle() {
|
||||
if (this.diffFile.isShowingFullFile) {
|
||||
|
|
|
@ -3,7 +3,12 @@ import { get } from 'lodash';
|
|||
import { DEFAULT_VARIANT, CANDIDATE_VARIANT, TRACKING_CONTEXT_SCHEMA } from './constants';
|
||||
|
||||
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) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<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 { retrieveAlert } from '~/jira_connect/subscriptions/utils';
|
||||
import { SET_ALERT } from '../store/mutation_types';
|
||||
|
@ -13,6 +14,7 @@ export default {
|
|||
GlAlert,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
GlEmptyState,
|
||||
SubscriptionsList,
|
||||
AddNamespaceButton,
|
||||
SignInButton,
|
||||
|
@ -21,12 +23,18 @@ export default {
|
|||
usersPath: {
|
||||
default: '',
|
||||
},
|
||||
subscriptions: {
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['alert']),
|
||||
shouldShowAlert() {
|
||||
return Boolean(this.alert?.message);
|
||||
},
|
||||
hasSubscriptions() {
|
||||
return !isEmpty(this.subscriptions);
|
||||
},
|
||||
userSignedIn() {
|
||||
return Boolean(!this.usersPath);
|
||||
},
|
||||
|
@ -66,15 +74,44 @@ export default {
|
|||
</template>
|
||||
</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">
|
||||
<div class="gl-display-flex gl-justify-content-end">
|
||||
<sign-in-button v-if="!userSignedIn" :users-path="usersPath" />
|
||||
<add-namespace-button v-else />
|
||||
</div>
|
||||
|
||||
<subscriptions-list />
|
||||
<subscriptions-list />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="!userSignedIn" class="gl-text-center">
|
||||
<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">
|
||||
{{ __('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>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
|
||||
import { GlButton, GlTable } from '@gitlab/ui';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { mapMutations } from 'vuex';
|
||||
import { removeSubscription } from '~/jira_connect/subscriptions/api';
|
||||
|
@ -12,7 +12,6 @@ import GroupItemName from './group_item_name.vue';
|
|||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlEmptyState,
|
||||
GlTable,
|
||||
GroupItemName,
|
||||
TimeagoTooltip,
|
||||
|
@ -44,17 +43,15 @@ export default {
|
|||
},
|
||||
],
|
||||
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.'),
|
||||
},
|
||||
methods: {
|
||||
...mapMutations({
|
||||
setAlert: SET_ALERT,
|
||||
}),
|
||||
isEmpty,
|
||||
isUnlinkButtonDisabled(item) {
|
||||
return !isEmpty(item);
|
||||
},
|
||||
isLoadingItem(item) {
|
||||
return this.loadingItem === item;
|
||||
},
|
||||
|
@ -81,29 +78,22 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-empty-state
|
||||
v-if="isEmpty(subscriptions)"
|
||||
:title="$options.i18n.emptyTitle"
|
||||
:description="$options.i18n.emptyDescription"
|
||||
/>
|
||||
<gl-table v-else :items="subscriptions" :fields="$options.fields">
|
||||
<template #cell(name)="{ item }">
|
||||
<group-item-name :group="item.group" />
|
||||
</template>
|
||||
<template #cell(created_at)="{ item }">
|
||||
<timeago-tooltip :time="item.created_at" />
|
||||
</template>
|
||||
<template #cell(actions)="{ item }">
|
||||
<gl-button
|
||||
:class="unlinkBtnClass(item)"
|
||||
category="secondary"
|
||||
:loading="isLoadingItem(item)"
|
||||
:disabled="!isEmpty(loadingItem)"
|
||||
@click.prevent="onClick(item)"
|
||||
>{{ __('Unlink') }}</gl-button
|
||||
>
|
||||
</template>
|
||||
</gl-table>
|
||||
</div>
|
||||
<gl-table :items="subscriptions" :fields="$options.fields">
|
||||
<template #cell(name)="{ item }">
|
||||
<group-item-name :group="item.group" />
|
||||
</template>
|
||||
<template #cell(created_at)="{ item }">
|
||||
<timeago-tooltip :time="item.created_at" />
|
||||
</template>
|
||||
<template #cell(actions)="{ item }">
|
||||
<gl-button
|
||||
:class="unlinkBtnClass(item)"
|
||||
category="secondary"
|
||||
:loading="isLoadingItem(item)"
|
||||
:disabled="isUnlinkButtonDisabled(loadingItem)"
|
||||
@click.prevent="onClick(item)"
|
||||
>{{ __('Unlink') }}</gl-button
|
||||
>
|
||||
</template>
|
||||
</gl-table>
|
||||
</template>
|
||||
|
|
|
@ -10,8 +10,8 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
|||
import eventHub from '../../event_hub';
|
||||
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
|
||||
import getStateQuery from '../../queries/get_state.query.graphql';
|
||||
import workInProgressQuery from '../../queries/states/work_in_progress.query.graphql';
|
||||
import removeWipMutation from '../../queries/toggle_wip.mutation.graphql';
|
||||
import draftQuery from '../../queries/states/draft.query.graphql';
|
||||
import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
|
||||
import StatusIcon from '../mr_widget_status_icon.vue';
|
||||
|
||||
export default {
|
||||
|
@ -23,7 +23,7 @@ export default {
|
|||
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
|
||||
apollo: {
|
||||
userPermissions: {
|
||||
query: workInProgressQuery,
|
||||
query: draftQuery,
|
||||
skip() {
|
||||
return !this.glFeatures.mergeRequestWidgetGraphql;
|
||||
},
|
||||
|
@ -53,25 +53,25 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
removeWipMutation() {
|
||||
removeDraftMutation() {
|
||||
const { mergeRequestQueryVariables } = this;
|
||||
|
||||
this.isMakingRequest = true;
|
||||
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: removeWipMutation,
|
||||
mutation: removeDraftMutation,
|
||||
variables: {
|
||||
...mergeRequestQueryVariables,
|
||||
wip: false,
|
||||
draft: false,
|
||||
},
|
||||
update(
|
||||
store,
|
||||
{
|
||||
data: {
|
||||
mergeRequestSetWip: {
|
||||
mergeRequestSetDraft: {
|
||||
errors,
|
||||
mergeRequest: { mergeableDiscussionsState, workInProgress, title },
|
||||
mergeRequest: { mergeableDiscussionsState, draft, title },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -91,7 +91,7 @@ export default {
|
|||
|
||||
const data = produce(sourceData, (draftState) => {
|
||||
draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState;
|
||||
draftState.project.mergeRequest.workInProgress = workInProgress;
|
||||
draftState.project.mergeRequest.draft = draft;
|
||||
draftState.project.mergeRequest.title = title;
|
||||
});
|
||||
|
||||
|
@ -104,14 +104,14 @@ export default {
|
|||
optimisticResponse: {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
__typename: 'Mutation',
|
||||
mergeRequestSetWip: {
|
||||
mergeRequestSetDraft: {
|
||||
__typename: 'MergeRequestSetWipPayload',
|
||||
errors: [],
|
||||
mergeRequest: {
|
||||
__typename: 'MergeRequest',
|
||||
mergeableDiscussionsState: true,
|
||||
title: this.mr.title,
|
||||
workInProgress: false,
|
||||
draft: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -119,7 +119,7 @@ export default {
|
|||
.then(
|
||||
({
|
||||
data: {
|
||||
mergeRequestSetWip: {
|
||||
mergeRequestSetDraft: {
|
||||
mergeRequest: { title },
|
||||
},
|
||||
},
|
||||
|
@ -137,9 +137,9 @@ export default {
|
|||
this.isMakingRequest = false;
|
||||
});
|
||||
},
|
||||
handleRemoveWIP() {
|
||||
handleRemoveDraft() {
|
||||
if (this.glFeatures.mergeRequestWidgetGraphql) {
|
||||
this.removeWipMutation();
|
||||
this.removeDraftMutation();
|
||||
} else {
|
||||
this.isMakingRequest = true;
|
||||
this.service
|
||||
|
@ -178,8 +178,8 @@ export default {
|
|||
size="small"
|
||||
:disabled="isMakingRequest"
|
||||
:loading="isMakingRequest"
|
||||
class="js-remove-wip gl-ml-3"
|
||||
@click="handleRemoveWIP"
|
||||
class="js-remove-draft gl-ml-3"
|
||||
@click="handleRemoveDraft"
|
||||
>
|
||||
{{ s__('mrWidget|Mark as ready') }}
|
||||
</gl-button>
|
||||
|
|
|
@ -23,7 +23,7 @@ query getState($projectPath: ID!, $iid: String!) {
|
|||
userPermissions {
|
||||
canMerge
|
||||
}
|
||||
workInProgress
|
||||
draft
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -17,8 +17,8 @@ export default function deviseState() {
|
|||
return stateKey.rebase;
|
||||
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
|
||||
return stateKey.pipelineFailed;
|
||||
} else if (this.workInProgress) {
|
||||
return stateKey.workInProgress;
|
||||
} else if (this.draft) {
|
||||
return stateKey.draft;
|
||||
} else if (this.hasMergeableDiscussionsState && !this.autoMergeEnabled) {
|
||||
return stateKey.unresolvedDiscussions;
|
||||
} else if (this.isPipelineBlocked) {
|
||||
|
|
|
@ -164,7 +164,7 @@ export default class MergeRequestStore {
|
|||
this.projectArchived = data.project_archived;
|
||||
this.isSHAMismatch = this.sha !== data.diff_head_sha;
|
||||
this.shouldBeRebased = Boolean(data.should_be_rebased);
|
||||
this.workInProgress = data.work_in_progress;
|
||||
this.draft = data.draft;
|
||||
}
|
||||
|
||||
const currentUser = data.current_user;
|
||||
|
@ -207,7 +207,7 @@ export default class MergeRequestStore {
|
|||
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
|
||||
this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
|
||||
this.shouldBeRebased = mergeRequest.shouldBeRebased;
|
||||
this.workInProgress = mergeRequest.workInProgress;
|
||||
this.draft = mergeRequest.draft;
|
||||
this.mergeRequestState = mergeRequest.state;
|
||||
|
||||
this.setState();
|
||||
|
|
|
@ -4,7 +4,7 @@ export const stateToComponentMap = {
|
|||
merging: 'mr-widget-merging',
|
||||
conflicts: 'mr-widget-conflicts',
|
||||
missingBranch: 'mr-widget-missing-branch',
|
||||
workInProgress: 'mr-widget-wip',
|
||||
draft: 'mr-widget-wip',
|
||||
readyToMerge: 'mr-widget-ready-to-merge',
|
||||
nothingToMerge: 'mr-widget-nothing-to-merge',
|
||||
notAllowedToMerge: 'mr-widget-not-allowed',
|
||||
|
@ -24,7 +24,7 @@ export const stateToComponentMap = {
|
|||
export const statesToShowHelpWidget = [
|
||||
'merging',
|
||||
'conflicts',
|
||||
'workInProgress',
|
||||
'draft',
|
||||
'readyToMerge',
|
||||
'checking',
|
||||
'unresolvedDiscussions',
|
||||
|
@ -40,7 +40,7 @@ export const stateKey = {
|
|||
nothingToMerge: 'nothingToMerge',
|
||||
checking: 'checking',
|
||||
conflicts: 'conflicts',
|
||||
workInProgress: 'workInProgress',
|
||||
draft: 'draft',
|
||||
pipelineFailed: 'pipelineFailed',
|
||||
unresolvedDiscussions: 'unresolvedDiscussions',
|
||||
pipelineBlocked: 'pipelineBlocked',
|
||||
|
|
|
@ -42,8 +42,6 @@ $header-height: 40px;
|
|||
|
||||
.jira-connect-app-body {
|
||||
max-width: 768px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
// needed for external_link
|
||||
|
|
|
@ -113,8 +113,6 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
|
|||
end
|
||||
|
||||
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['Content-Length'] = manifest.size
|
||||
response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
|
||||
|
|
|
@ -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
|
|
@ -53,9 +53,6 @@ module Types
|
|||
description: 'Indicates if the source branch is protected.'
|
||||
field :target_branch, GraphQL::Types::String, null: false,
|
||||
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,
|
||||
description: 'Indicates if the merge request is a draft.'
|
||||
field :merge_when_pipeline_succeeds, GraphQL::Types::Boolean, null: true,
|
||||
|
|
|
@ -65,9 +65,6 @@ module Types
|
|||
mount_mutation Mutations::MergeRequests::SetLocked
|
||||
mount_mutation Mutations::MergeRequests::SetMilestone
|
||||
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::SetAssignees
|
||||
mount_mutation Mutations::MergeRequests::ReviewerRereview
|
||||
|
|
|
@ -5,10 +5,11 @@ module TtlExpirable
|
|||
|
||||
included do
|
||||
validates :status, presence: true
|
||||
default_value_for :read_at, Time.zone.now
|
||||
|
||||
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 :lock_next_by, ->(sort) do
|
||||
|
@ -17,4 +18,8 @@ module TtlExpirable
|
|||
.lock('FOR UPDATE SKIP LOCKED')
|
||||
end
|
||||
end
|
||||
|
||||
def read!
|
||||
self.update(read_at: Time.zone.now)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,7 +12,8 @@ class GpgSignature < ApplicationRecord
|
|||
same_user_different_email: 2,
|
||||
other_user: 3,
|
||||
unverified_key: 4,
|
||||
unknown_key: 5
|
||||
unknown_key: 5,
|
||||
multiple_signatures: 6
|
||||
}
|
||||
|
||||
belongs_to :project
|
||||
|
|
|
@ -268,7 +268,6 @@ class MergeRequest < ApplicationRecord
|
|||
from_fork.where('source_project_id = ? OR target_project_id = ?', project.id, project.id)
|
||||
end
|
||||
scope :merged, -> { with_state(:merged) }
|
||||
scope :closed_and_merged, -> { with_states(:closed, :merged) }
|
||||
scope :open_and_closed, -> { with_states(:opened, :closed) }
|
||||
scope :drafts, -> { where(draft: true) }
|
||||
scope :from_source_branches, ->(branches) { where(source_branch: branches) }
|
||||
|
|
|
@ -497,6 +497,10 @@ class Namespace < ApplicationRecord
|
|||
Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
def project_namespace_creation_enabled?
|
||||
Feature.enabled?(:create_project_namespace_on_project_create, self, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expire_child_caches
|
||||
|
|
|
@ -4,8 +4,6 @@ module Namespaces
|
|||
class ProjectNamespace < Namespace
|
||||
has_one :project, foreign_key: :project_namespace_id, inverse_of: :project_namespace
|
||||
|
||||
validates :project, presence: true
|
||||
|
||||
def self.sti_name
|
||||
'Project'
|
||||
end
|
||||
|
|
|
@ -9,5 +9,9 @@ module Packages
|
|||
|
||||
package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first
|
||||
end
|
||||
|
||||
def self.table_name_prefix
|
||||
'packages_npm_'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -39,6 +39,7 @@ class Packages::Package < ApplicationRecord
|
|||
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 :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 :pipelines, through: :build_infos, disable_joins: -> { disable_cross_joins_to_pipelines? }
|
||||
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 })
|
||||
end
|
||||
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) }
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ class Project < ApplicationRecord
|
|||
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
|
||||
|
||||
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?
|
||||
|
||||
|
@ -147,7 +147,7 @@ class Project < ApplicationRecord
|
|||
belongs_to :namespace
|
||||
# Sync deletion via DB Trigger to ensure we do not have
|
||||
# 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_attribute :parent_id, :namespace_id
|
||||
|
||||
|
@ -476,6 +476,7 @@ class Project < ApplicationRecord
|
|||
validates :project_feature, 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 :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
|
||||
ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS },
|
||||
|
@ -2919,12 +2920,28 @@ class Project < ApplicationRecord
|
|||
end
|
||||
|
||||
def ensure_project_namespace_in_sync
|
||||
if changes.keys & [:name, :path, :namespace_id, :visibility_level] && project_namespace.present?
|
||||
project_namespace.name = name
|
||||
project_namespace.path = path
|
||||
project_namespace.parent = namespace
|
||||
project_namespace.visibility_level = visibility_level
|
||||
end
|
||||
# create project_namespace when project is created if create_project_namespace_on_project_create FF is enabled
|
||||
build_project_namespace if project_namespace_creation_enabled?
|
||||
|
||||
# regardless of create_project_namespace_on_project_create FF we need
|
||||
# to keep project and project namespace in sync if there is one
|
||||
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
|
||||
|
||||
|
|
|
@ -1091,6 +1091,13 @@ class Repository
|
|||
after_create
|
||||
|
||||
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
|
||||
|
||||
def create_from_bundle(bundle_path)
|
||||
|
|
|
@ -5,26 +5,37 @@ module Packages
|
|||
class PackagePresenter
|
||||
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
|
||||
|
||||
def initialize(name, packages)
|
||||
def initialize(name, packages, include_metadata: false)
|
||||
@name = name
|
||||
@packages = packages
|
||||
@include_metadata = include_metadata
|
||||
end
|
||||
|
||||
def versions
|
||||
package_versions = {}
|
||||
|
||||
packages.each_batch do |relation|
|
||||
relation.including_dependency_links
|
||||
.preload_files
|
||||
.each do |package|
|
||||
package_file = package.package_files.last
|
||||
batched_packages = relation.including_dependency_links
|
||||
.preload_files
|
||||
|
||||
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)
|
||||
end
|
||||
batched_packages.each do |package|
|
||||
package_file = package.package_files.last
|
||||
|
||||
next unless package_file
|
||||
|
||||
package_versions[package.version] = build_package_version(package, package_file)
|
||||
end
|
||||
end
|
||||
|
||||
package_versions
|
||||
|
@ -41,14 +52,14 @@ module Packages
|
|||
end
|
||||
|
||||
def build_package_version(package, package_file)
|
||||
{
|
||||
abbreviated_package_json(package).merge(
|
||||
name: package.name,
|
||||
version: package.version,
|
||||
dist: {
|
||||
shasum: package_file.file_sha1,
|
||||
tarball: tarball_url(package, package_file)
|
||||
}
|
||||
}.tap do |package_version|
|
||||
).tap do |package_version|
|
||||
package_version.merge!(build_package_dependencies(package))
|
||||
end
|
||||
end
|
||||
|
@ -79,6 +90,13 @@ module Packages
|
|||
Packages::Tag.for_packages(packages)
|
||||
.preload_package
|
||||
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
|
||||
|
|
|
@ -30,8 +30,7 @@ module DependencyProxy
|
|||
blob.save!
|
||||
end
|
||||
|
||||
# Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
|
||||
blob.touch if from_cache
|
||||
blob.read! if from_cache
|
||||
success(blob: blob, from_cache: from_cache)
|
||||
end
|
||||
|
||||
|
|
|
@ -58,6 +58,8 @@ module DependencyProxy
|
|||
|
||||
def respond(from_cache: true)
|
||||
if @manifest
|
||||
@manifest.read!
|
||||
|
||||
success(manifest: @manifest, from_cache: from_cache)
|
||||
else
|
||||
error('Failed to download the manifest from the external registry', 503)
|
||||
|
|
|
@ -21,6 +21,10 @@ module Packages
|
|||
::Packages::CreateDependencyService.new(package, package_dependencies).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
|
||||
end
|
||||
|
||||
|
|
|
@ -152,14 +152,12 @@ module Projects
|
|||
|
||||
deleted_count = project.commit_statuses.delete_all
|
||||
|
||||
if deleted_count > 0
|
||||
Gitlab::AppLogger.info(
|
||||
class: 'Projects::DestroyService',
|
||||
project_id: project.id,
|
||||
message: 'leftover commit statuses',
|
||||
orphaned_commit_status_count: deleted_count
|
||||
)
|
||||
end
|
||||
Gitlab::AppLogger.info(
|
||||
class: 'Projects::DestroyService',
|
||||
project_id: project.id,
|
||||
message: 'leftover commit statuses',
|
||||
orphaned_commit_status_count: deleted_count
|
||||
)
|
||||
end
|
||||
|
||||
# The project can have multiple webhooks with hundreds of thousands of web_hook_logs.
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -23,7 +23,7 @@
|
|||
= form_tag personal_access_token_import_github_path, method: :post do
|
||||
.form-group
|
||||
%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
|
||||
= import_github_personal_access_token_message
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
%main.jira-connect-app.gl-px-5.gl-pt-7.gl-mx-auto
|
||||
- if current_user.blank? && @subscriptions.empty?
|
||||
.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.')
|
||||
.js-jira-connect-app{ data: jira_connect_app_data(@subscriptions) }
|
||||
|
||||
.gl-mt-7
|
||||
= 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
|
||||
%p.jira-connect-app-body.gl-px-5.gl-font-base.gl-text-center.gl-mx-auto
|
||||
%strong= s_('Integrations|Browser limitations')
|
||||
- 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/' }
|
||||
|
|
|
@ -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};
|
|
@ -16,4 +16,5 @@
|
|||
|
||||
= render 'layouts/img_loader'
|
||||
|
||||
= render 'layouts/published_experiments'
|
||||
= yield :scripts_body
|
||||
|
|
|
@ -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
|
|
@ -1,5 +1,4 @@
|
|||
- add_page_startup_api_call discussions_path(@issue)
|
||||
- add_page_startup_api_call notes_url
|
||||
|
||||
- @gfm_form = true
|
||||
|
||||
|
|
|
@ -246,6 +246,15 @@
|
|||
:weight: 1
|
||||
:idempotent: true
|
||||
: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
|
||||
:worker_name: ContainerExpirationPolicyWorker
|
||||
:feature_category: :container_registry
|
||||
|
@ -1087,15 +1096,6 @@
|
|||
:idempotent:
|
||||
:tags:
|
||||
- :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
|
||||
:worker_name: IncidentManagement::AddSeveritySystemNoteWorker
|
||||
:feature_category: :incident_management
|
||||
|
|
|
@ -12,7 +12,6 @@ module Clusters
|
|||
include CronjobQueue
|
||||
# rubocop:enable Scalability/CronWorkerContext
|
||||
|
||||
queue_namespace :incident_management
|
||||
feature_category :incident_management
|
||||
urgency :low
|
||||
|
||||
|
|
|
@ -13,9 +13,8 @@ module DependencyProxy
|
|||
|
||||
def perform
|
||||
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.updated_before(policy.ttl)
|
||||
qualified_manifests = policy.group.dependency_proxy_manifests.active.updated_before(policy.ttl)
|
||||
qualified_blobs = policy.group.dependency_proxy_blobs.active.read_before(policy.ttl)
|
||||
qualified_manifests = policy.group.dependency_proxy_manifests.active.read_before(policy.ttl)
|
||||
|
||||
enqueue_blob_cleanup_job if expire_artifacts(qualified_blobs, DependencyProxy::Blob)
|
||||
enqueue_manifest_cleanup_job if expire_artifacts(qualified_manifests, DependencyProxy::Manifest)
|
||||
|
|
|
@ -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
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: rate_limiter_safe_increment
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73343
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285352
|
||||
name: multiple_gpg_signatures
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74095
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345261
|
||||
milestone: '14.5'
|
||||
type: development
|
||||
group: group::project management
|
||||
group: group::source code
|
||||
default_enabled: false
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: linear_group_plans_preloaded_ancestor_scopes
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70685
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341349
|
||||
milestone: '14.4'
|
||||
name: packages_npm_abbreviated_metadata
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73639
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344827
|
||||
milestone: '14.5'
|
||||
type: development
|
||||
group: group::access
|
||||
group: group::package
|
||||
default_enabled: false
|
|
@ -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']['cron'] ||= '* 0/15 * * *'
|
||||
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
|
||||
Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({})
|
||||
|
|
|
@ -35,8 +35,4 @@ unless Gitlab.jh?
|
|||
])
|
||||
end
|
||||
|
||||
begin
|
||||
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
|
||||
Gitlab::Database::Partitioning.sync_partitions_ignore_db_error
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
# and what types we expect those attribute values to be.
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
|
|
@ -157,16 +157,17 @@ class Gitlab::Seeder::CycleAnalytics
|
|||
end
|
||||
|
||||
def create_new_vsm_project
|
||||
namespace = FactoryBot.create(
|
||||
:group,
|
||||
name: "Value Stream Management Group #{suffix}",
|
||||
path: "vsmg-#{suffix}"
|
||||
)
|
||||
project = FactoryBot.create(
|
||||
:project,
|
||||
name: "Value Stream Management Project #{suffix}",
|
||||
path: "vsmp-#{suffix}",
|
||||
creator: admin,
|
||||
namespace: FactoryBot.create(
|
||||
:group,
|
||||
name: "Value Stream Management Group #{suffix}",
|
||||
path: "vsmg-#{suffix}"
|
||||
)
|
||||
namespace: namespace
|
||||
)
|
||||
|
||||
project.create_repository
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
50a5c8af2cde1ae79d627f70d3b266488f76f76b481aefca8516db5360cfa843
|
|
@ -0,0 +1 @@
|
|||
13cf3d164d541df48b6d14d7cc1953113476ba8ea5975d7d0c5f84098e2e0e61
|
|
@ -0,0 +1 @@
|
|||
a48f62bed7e4c4a0e69acd3b340065317aff71602e696970276a4e443f1dcabf
|
|
@ -0,0 +1 @@
|
|||
a2556a3d8b21e59caa6cbf7f83d621fef391904d0c13c77c0e5da713a580b4c9
|
|
@ -13237,7 +13237,8 @@ CREATE TABLE dependency_proxy_blobs (
|
|||
file_store integer,
|
||||
file_name character varying 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
|
||||
|
@ -13286,6 +13287,7 @@ CREATE TABLE dependency_proxy_manifests (
|
|||
digest text NOT NULL,
|
||||
content_type text,
|
||||
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_167a9a8a91 CHECK ((char_length(content_type) <= 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;
|
||||
|
||||
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 (
|
||||
dependency_link_id bigint NOT NULL,
|
||||
target_framework text NOT NULL,
|
||||
|
@ -23524,6 +23532,9 @@ ALTER TABLE ONLY packages_helm_file_metadata
|
|||
ALTER TABLE ONLY packages_maven_metadata
|
||||
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
|
||||
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_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_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);
|
||||
|
||||
|
@ -30779,6 +30790,9 @@ ALTER TABLE ONLY atlassian_identities
|
|||
ALTER TABLE ONLY serverless_domain_cluster
|
||||
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
|
||||
ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -3509,31 +3509,6 @@ Input type: `MergeRequestSetSubscriptionInput`
|
|||
| <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. |
|
||||
|
||||
### `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`
|
||||
|
||||
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="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="mergerequestworkinprogress"></a>`workInProgress` **{warning-solid}** | [`Boolean!`](#boolean) | **Deprecated** in 13.12. Use `draft`. |
|
||||
|
||||
#### Fields with arguments
|
||||
|
||||
|
|
|
@ -365,8 +365,7 @@ include: '.gitlab-ci-production.yml'
|
|||
|
||||
#### `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,
|
||||
use `include:file`. You can use `include:file` in combination with `include:project` only.
|
||||
|
@ -451,8 +450,6 @@ include:
|
|||
|
||||
#### `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).
|
||||
|
||||
**Keyword type**: Global keyword.
|
||||
|
@ -643,8 +640,7 @@ job2:
|
|||
|
||||
**Additional details**:
|
||||
|
||||
You might need to use single quotes (`'`) or double quotes (`"`) when using
|
||||
[special characters in `script`](script.md#use-special-characters-with-script).
|
||||
- When you use [these special characters in `script`](script.md#use-special-characters-with-script), you must use single quotes (`'`) or double quotes (`"`) .
|
||||
|
||||
**Related topics**:
|
||||
|
||||
|
@ -680,8 +676,8 @@ job:
|
|||
|
||||
**Additional details**:
|
||||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
**Related topics**:
|
||||
|
||||
|
@ -799,7 +795,7 @@ job4:
|
|||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
|
@ -834,7 +830,7 @@ job2:
|
|||
|
||||
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`.
|
||||
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`.
|
||||
|
||||
|
@ -865,8 +861,6 @@ job2:
|
|||
|
||||
### `extends`
|
||||
|
||||
> Introduced in GitLab 11.3.
|
||||
|
||||
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
|
||||
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). |
|
||||
| `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). |
|
||||
| `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`**:
|
||||
|
||||
|
@ -1440,8 +1434,6 @@ deploy:
|
|||
|
||||
#### `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,
|
||||
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
|
||||
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
|
||||
`.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
|
||||
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
|
||||
`.gitlab-ci.yml` file. You can't use variables defined in a `script` section.
|
||||
|
||||
|
@ -2124,7 +2116,7 @@ environment.
|
|||
|
||||
**Additional details**:
|
||||
|
||||
See [`environment:action`](#environmentaction) for more details and an example.
|
||||
- See [`environment:action`](#environmentaction) for more details and an example.
|
||||
|
||||
#### `environment:action`
|
||||
|
||||
|
@ -2392,7 +2384,7 @@ cache-job:
|
|||
|
||||
**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%`
|
||||
- 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`
|
||||
use the new cache, instead of rebuilding the dependencies.
|
||||
|
||||
**Additional details**: 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`.
|
||||
**Additional details**:
|
||||
|
||||
- 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`
|
||||
|
||||
|
@ -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`
|
||||
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,
|
||||
the prefix is added to the `default` key.
|
||||
**Additional details**:
|
||||
|
||||
- If no file in `cache:key:files` is changed in any commits, the prefix is added to the `default` key.
|
||||
|
||||
#### `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.
|
||||
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
|
||||
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:
|
||||
|
||||
- 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
|
||||
`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 `%`:
|
||||
|
||||
```yaml
|
||||
|
@ -2882,7 +2877,7 @@ job:
|
|||
- 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:`:
|
||||
|
||||
```yaml
|
||||
|
@ -2900,9 +2895,8 @@ link outside it. You can use Wildcards that use [glob](https://en.wikipedia.org/
|
|||
patterns and:
|
||||
|
||||
- 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).
|
||||
- In GitLab Runner 12.10 and earlier,
|
||||
[`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
|
||||
[`doublestar.Glob`](https://pkg.go.dev/github.com/bmatcuk/doublestar@v1.2.2?tab=doc#Match).
|
||||
- In GitLab Runner 12.10 and earlier, [`filepath.Match`](https://pkg.go.dev/path/filepath#Match).
|
||||
|
||||
To restrict which jobs a specific job fetches artifacts from, see [dependencies](#dependencies).
|
||||
|
||||
|
@ -2983,9 +2977,6 @@ artifacts:
|
|||
|
||||
#### `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:
|
||||
|
||||
- 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)**
|
||||
|
||||
> - Introduced in GitLab 11.5.
|
||||
> - 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.
|
||||
> [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)
|
||||
as artifacts.
|
||||
|
@ -3064,8 +3053,7 @@ dashboards.
|
|||
|
||||
##### `artifacts:reports:cobertura`
|
||||
|
||||
> - [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.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9.
|
||||
|
||||
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
|
||||
|
@ -3076,9 +3064,7 @@ third party ports for other languages like JavaScript, Python, Ruby, and so on.
|
|||
|
||||
##### `artifacts:reports:codequality`
|
||||
|
||||
> - Introduced in GitLab 11.5.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) to GitLab Free in 13.2.
|
||||
> - Requires GitLab Runner 11.5 and above.
|
||||
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) to GitLab Free in 13.2.
|
||||
|
||||
The `codequality` report collects [Code Quality issues](../../user/project/merge_requests/code_quality.md)
|
||||
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)**
|
||||
|
||||
> - 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)
|
||||
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)**
|
||||
|
||||
> - Introduced in GitLab 11.5.
|
||||
> - Requires GitLab Runner 11.5 and above.
|
||||
|
||||
The `dast` report collects [DAST vulnerabilities](../../user/application_security/dast/index.md)
|
||||
as artifacts.
|
||||
|
||||
|
@ -3121,9 +3101,6 @@ dashboards.
|
|||
|
||||
##### `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)
|
||||
as artifacts.
|
||||
|
||||
|
@ -3132,15 +3109,14 @@ dashboards.
|
|||
|
||||
##### `artifacts:reports:dotenv`
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17066) in GitLab 12.9.
|
||||
> - Requires GitLab Runner 11.5 and later.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17066) in GitLab 12.9.
|
||||
|
||||
The `dotenv` report collects a set of environment variables as artifacts.
|
||||
|
||||
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).
|
||||
|
||||
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 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`
|
||||
|
||||
> - [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)
|
||||
as artifacts. Although JUnit was originally developed in Java, there are many
|
||||
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.
|
||||
|
||||
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`),
|
||||
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]`).
|
||||
|
||||
##### `artifacts:reports:license_scanning` **(ULTIMATE)**
|
||||
|
||||
> - Introduced in GitLab 12.8.
|
||||
> - Requires GitLab Runner 11.5 and above.
|
||||
> Introduced in GitLab 12.8.
|
||||
|
||||
The `license_scanning` report collects [Licenses](../../user/compliance/license_compliance/index.md)
|
||||
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
|
||||
dashboards.
|
||||
The License Compliance report uploads to GitLab as an artifact and displays automatically
|
||||
in merge requests and the pipeline view. The report provides data for security dashboards.
|
||||
|
||||
##### `artifacts:reports:load_performance` **(PREMIUM)**
|
||||
|
||||
|
@ -3208,8 +3180,6 @@ shown in merge requests automatically.
|
|||
|
||||
##### `artifacts:reports:metrics` **(PREMIUM)**
|
||||
|
||||
> Introduced in GitLab 11.10.
|
||||
|
||||
The `metrics` report collects [Metrics](../metrics_reports.md)
|
||||
as artifacts.
|
||||
|
||||
|
@ -3218,7 +3188,6 @@ The collected Metrics report uploads to GitLab as an artifact and displays in me
|
|||
##### `artifacts:reports:requirements` **(ULTIMATE)**
|
||||
|
||||
> - [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.
|
||||
|
||||
|
@ -3228,9 +3197,7 @@ marked as Satisfied.
|
|||
|
||||
##### `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.
|
||||
> - Requires GitLab Runner 11.5 and above.
|
||||
|
||||
The `sast` report collects [SAST vulnerabilities](../../user/application_security/sast/index.md)
|
||||
as artifacts.
|
||||
|
@ -3302,7 +3269,7 @@ failure.
|
|||
|
||||
1. `on_success` (default): Upload artifacts only when the job succeeds.
|
||||
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
|
||||
troubleshoot failing tests.
|
||||
|
||||
|
@ -3394,8 +3361,6 @@ to select a specific site profile and scanner profile.
|
|||
|
||||
### `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.
|
||||
If not defined, defaults to `0` and jobs do not retry.
|
||||
|
||||
|
@ -3513,8 +3478,6 @@ test:
|
|||
|
||||
### `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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ end
|
|||
```
|
||||
|
||||
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:
|
||||
|
||||
- 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.
|
||||
|
||||
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)
|
||||
so you can use it when resolving some concepts around experimentation in the client layer.
|
||||
|
||||
### Use experiments in Vue
|
||||
|
||||
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:
|
||||
|
||||
```ruby
|
||||
|
@ -587,7 +587,51 @@ For example, the Vue component for the previously-defined `pill_color` experimen
|
|||
```
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -588,8 +588,6 @@ class like so:
|
|||
|
||||
```ruby
|
||||
class MyMigration < Gitlab::Database::Migration[1.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_name'
|
||||
|
@ -633,8 +631,6 @@ be used with a name option. For example:
|
|||
|
||||
```ruby
|
||||
class MyMigration < Gitlab::Database::Migration[1.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
INDEX_NAME = 'index_name'
|
||||
|
||||
def up
|
||||
|
|
|
@ -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)
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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>"
|
||||
```
|
||||
|
||||
### npm dependencies metadata
|
||||
### npm metadata
|
||||
|
||||
> - [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.
|
||||
> - [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
|
||||
- version
|
||||
- dist-tags
|
||||
- dependencies
|
||||
- dependencies
|
||||
- devDependencies
|
||||
- bundleDependencies
|
||||
- peerDependencies
|
||||
- deprecated
|
||||
- `name`
|
||||
- `versions`
|
||||
- `name`
|
||||
- `version`
|
||||
- `deprecated`
|
||||
- `dependencies`
|
||||
- `devDependencies`
|
||||
- `bundleDependencies`
|
||||
- `peerDependencies`
|
||||
- `bin`
|
||||
- `directories`
|
||||
- `dist`
|
||||
- `engines`
|
||||
- `_hasShrinkwrap`
|
||||
|
||||
## 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,
|
||||
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`
|
||||
|
||||
This is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/238950) in GitLab
|
||||
|
|
|
@ -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
|
||||
---
|
||||
|
||||
# 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:
|
||||
|
||||
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 the checkbox under **Enable integration**.
|
||||
1. Fill in the required fields:
|
||||
|
||||
- **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`.
|
||||
GitLab replaces `:id` with the issue number (for example,
|
||||
`https://customissuetracker.com/project-name/:id`, which becomes `https://customissuetracker.com/project-name/123`).
|
||||
GitLab replaces `:id` with the issue number (for example,
|
||||
`https://customissuetracker.com/project-name/:id`, which becomes
|
||||
`https://customissuetracker.com/project-name/123`).
|
||||
- **New issue URL**:
|
||||
<!-- 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.**
|
||||
Enter any URL here.
|
||||
For more information, see [issue 327503](https://gitlab.com/gitlab-org/gitlab/-/issues/327503).
|
||||
**This URL is not used and an [issue exists](https://gitlab.com/gitlab-org/gitlab/-/issues/327503) to remove it.**
|
||||
Enter any URL.
|
||||
|
||||
1. Select **Save changes** or optionally select **Test settings**.
|
||||
|
||||
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.
|
||||
1. Optional. Select **Test settings**.
|
||||
1. Select **Save changes**.
|
||||
|
||||
## Reference issues in a custom issue tracker
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 6.5 KiB |
|
@ -207,7 +207,7 @@ To configure a custom mailbox for Service Desk with IMAP, add the following snip
|
|||
service_desk_email:
|
||||
enabled: true
|
||||
address: "project_contact+%{key}@example.com"
|
||||
user: "project_support@example.com"
|
||||
user: "project_contact@example.com"
|
||||
password: "[REDACTED]"
|
||||
host: "imap.gmail.com"
|
||||
port: 993
|
||||
|
@ -224,7 +224,7 @@ To configure a custom mailbox for Service Desk with IMAP, add the following snip
|
|||
```ruby
|
||||
gitlab_rails['service_desk_email_enabled'] = true
|
||||
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_mailbox_name'] = "inbox"
|
||||
gitlab_rails['service_desk_email_idle_timeout'] = 60
|
||||
|
|
|
@ -121,7 +121,9 @@ module API
|
|||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ module BulkImports
|
|||
relation_hash, relation_index = data
|
||||
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_index: relation_index,
|
||||
relation_sym: key.to_sym,
|
||||
|
@ -25,6 +25,9 @@ module BulkImports
|
|||
excluded_keys: import_export_config.relation_excluded_keys(key)
|
||||
)
|
||||
end
|
||||
|
||||
relation_object.assign_attributes(portable_class_sym => portable)
|
||||
relation_object
|
||||
end
|
||||
|
||||
def load(_, object)
|
||||
|
@ -94,6 +97,10 @@ module BulkImports
|
|||
def members_mapper
|
||||
@members_mapper ||= BulkImports::UsersMapper.new(context: context)
|
||||
end
|
||||
|
||||
def portable_class_sym
|
||||
portable.class.to_s.downcase.to_sym
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -79,28 +79,6 @@ module Gitlab
|
|||
increment(key, options[:scope]) > threshold_value
|
||||
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.
|
||||
# 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)
|
||||
#
|
||||
# @return [Integer] incremented value
|
||||
def safe_increment(key, scope)
|
||||
def increment(key, scope)
|
||||
interval_value = interval(key)
|
||||
|
||||
period_key, time_elapsed_in_period = Time.now.to_i.divmod(interval_value)
|
||||
|
|
|
@ -355,6 +355,7 @@ packages_dependency_links: :gitlab_main
|
|||
packages_events: :gitlab_main
|
||||
packages_helm_file_metadata: :gitlab_main
|
||||
packages_maven_metadata: :gitlab_main
|
||||
packages_npm_metadata: :gitlab_main
|
||||
packages_nuget_dependency_link_metadata: :gitlab_main
|
||||
packages_nuget_metadata: :gitlab_main
|
||||
packages_package_file_build_infos: :gitlab_main
|
||||
|
|
|
@ -31,6 +31,12 @@ module Gitlab
|
|||
registered_tables.merge(tables)
|
||||
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)
|
||||
Gitlab::AppLogger.info(message: 'Syncing dynamic postgres partitions')
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ module Gitlab
|
|||
EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'
|
||||
|
||||
NoRepository = Class.new(::Gitlab::Git::BaseError)
|
||||
RepositoryExists = Class.new(::Gitlab::Git::BaseError)
|
||||
InvalidRepository = Class.new(::Gitlab::Git::BaseError)
|
||||
InvalidBlobName = Class.new(::Gitlab::Git::BaseError)
|
||||
InvalidRef = Class.new(::Gitlab::Git::BaseError)
|
||||
|
@ -101,6 +102,8 @@ module Gitlab
|
|||
def create_repository
|
||||
wrapped_gitaly_errors do
|
||||
gitaly_repository_client.create_repository
|
||||
rescue GRPC::AlreadyExists => e
|
||||
raise RepositoryExists, e.message
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ module Gitlab
|
|||
|
||||
if gpg_key
|
||||
Gitlab::Gpg::CurrentKeyChain.add(gpg_key.key)
|
||||
clear_memoization(:verified_signature)
|
||||
clear_memoization(:gpg_signatures)
|
||||
end
|
||||
|
||||
yield gpg_key
|
||||
|
@ -56,16 +56,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def verified_signature
|
||||
strong_memoize(:verified_signature) { gpgme_signature }
|
||||
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
|
||||
gpg_signatures.first
|
||||
end
|
||||
|
||||
def create_cached_signature!
|
||||
|
@ -77,6 +68,24 @@ module Gitlab
|
|||
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)
|
||||
user_infos = user_infos(gpg_key)
|
||||
verification_status = verification_status(gpg_key)
|
||||
|
@ -93,6 +102,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
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 :unverified_key unless gpg_key.verified?
|
||||
return :unverified unless verified_signature&.valid?
|
||||
|
|
|
@ -23326,6 +23326,9 @@ msgstr ""
|
|||
msgid "No forks are available to you."
|
||||
msgstr ""
|
||||
|
||||
msgid "No group provided"
|
||||
msgstr ""
|
||||
|
||||
msgid "No grouping"
|
||||
msgstr ""
|
||||
|
||||
|
@ -23377,6 +23380,9 @@ msgstr ""
|
|||
msgid "No members found"
|
||||
msgstr ""
|
||||
|
||||
msgid "No memberships found"
|
||||
msgstr ""
|
||||
|
||||
msgid "No merge requests found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24442,6 +24448,9 @@ msgstr ""
|
|||
msgid "Package type must be Maven"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package type must be NPM"
|
||||
msgstr ""
|
||||
|
||||
msgid "Package type must be NuGet"
|
||||
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."
|
||||
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."
|
||||
msgstr ""
|
||||
|
||||
|
@ -39464,6 +39476,9 @@ msgstr ""
|
|||
msgid "You do not have permission to access dora metrics."
|
||||
msgstr ""
|
||||
|
||||
msgid "You do not have permission to approve a member"
|
||||
msgstr ""
|
||||
|
||||
msgid "You do not have permission to leave this %{namespaceType}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -41767,6 +41782,9 @@ msgstr ""
|
|||
msgid "starts on %{timebox_start_date}"
|
||||
msgstr ""
|
||||
|
||||
msgid "structure is too large"
|
||||
msgstr ""
|
||||
|
||||
msgid "stuck"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -19,4 +19,5 @@ Gem::Specification.new do |spec|
|
|||
spec.require_paths = ['lib']
|
||||
|
||||
spec.add_runtime_dependency 'chemlab', '~> 0.9'
|
||||
spec.add_runtime_dependency 'zeitwerk', '~> 2.4'
|
||||
end
|
||||
|
|
|
@ -1,31 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
module Gitlab
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -62,7 +62,7 @@ RSpec.describe Dashboard::TodosController do
|
|||
create(:issue, project: project, assignees: [user])
|
||||
group_2 = create(:group)
|
||||
group_2.add_owner(user)
|
||||
project_2 = create(:project)
|
||||
project_2 = create(:project, namespace: user.namespace)
|
||||
project_2.add_developer(user)
|
||||
merge_request_2 = create(:merge_request, source_project: project_2)
|
||||
create(:todo, project: project, author: author, user: user, target: merge_request_2)
|
||||
|
|
|
@ -86,10 +86,11 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
|||
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:user) { create(:user) }
|
||||
let(:maintainer) { true }
|
||||
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
project.add_maintainer(user) if maintainer
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
@ -383,8 +384,9 @@ RSpec.describe Projects::MergeRequests::DiffsController do
|
|||
end
|
||||
|
||||
context 'when the user cannot view the merge request' do
|
||||
let(:maintainer) { false }
|
||||
|
||||
before do
|
||||
project.team.truncate
|
||||
diff_for_path(old_path: existing_path, new_path: existing_path)
|
||||
end
|
||||
|
||||
|
|
|
@ -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(: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
|
||||
sign_in(user)
|
||||
|
@ -2073,8 +2074,6 @@ RSpec.describe Projects::MergeRequestsController do
|
|||
end
|
||||
|
||||
describe 'POST #rebase' do
|
||||
let(:viewer) { user }
|
||||
|
||||
def post_rebase
|
||||
post :rebase, params: { namespace_id: project.namespace, project_id: project, id: merge_request }
|
||||
end
|
||||
|
@ -2085,7 +2084,7 @@ RSpec.describe Projects::MergeRequestsController do
|
|||
|
||||
context 'successfully' do
|
||||
it 'enqeues a RebaseWorker' do
|
||||
expect_rebase_worker_for(viewer)
|
||||
expect_rebase_worker_for(user)
|
||||
|
||||
post_rebase
|
||||
|
||||
|
@ -2108,17 +2107,17 @@ RSpec.describe Projects::MergeRequestsController do
|
|||
context 'with a forked project' do
|
||||
let(:forked_project) { fork_project(project, fork_owner, repository: true) }
|
||||
let(:fork_owner) { create(:user) }
|
||||
|
||||
before do
|
||||
project.add_developer(fork_owner)
|
||||
|
||||
merge_request.update!(source_project: forked_project)
|
||||
forked_project.add_reporter(user)
|
||||
end
|
||||
let(:merge_request_source_project) { forked_project }
|
||||
|
||||
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
|
||||
expect_rebase_worker_for(viewer).never
|
||||
expect_rebase_worker_for(user).never
|
||||
|
||||
post_rebase
|
||||
|
||||
|
|
|
@ -499,13 +499,12 @@ RSpec.describe RegistrationsController do
|
|||
expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}")
|
||||
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|
|
||||
m.call(*args)
|
||||
|
||||
expect(Gitlab::ApplicationContext.current)
|
||||
.to include('meta.user' => base_user_params[:username],
|
||||
'meta.caller_id' => 'RegistrationsController#create')
|
||||
.to include('meta.caller_id' => 'RegistrationsController#create')
|
||||
end
|
||||
|
||||
subject
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
FactoryBot.define do
|
||||
factory :project_namespace, class: 'Namespaces::ProjectNamespace' do
|
||||
project
|
||||
association :project, factory: :project, strategy: :build
|
||||
parent { project.namespace }
|
||||
visibility_level { project.visibility_level }
|
||||
name { project.name }
|
||||
|
|
|
@ -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
|
|
@ -115,6 +115,19 @@ RSpec.describe 'GPG signed commits' do
|
|||
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
|
||||
user_1_key
|
||||
|
||||
|
@ -169,7 +182,7 @@ RSpec.describe 'GPG signed commits' do
|
|||
page.find('.gpg-status-box', text: 'Unverified').click
|
||||
|
||||
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
|
||||
|
|
|
@ -36,11 +36,11 @@
|
|||
".{1,}": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"deprecated": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
".{1,}": { "type": "string" }
|
||||
}
|
||||
}
|
||||
"deprecated": { "type": "string"},
|
||||
"bin": { "type": "string" },
|
||||
"directories": { "type": "array" },
|
||||
"engines": { "type": "object" },
|
||||
"_hasShrinkwrap": { "type": "boolean" },
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
"express":"^4.16.4"
|
||||
},
|
||||
"dist":{
|
||||
"shasum":"f572d396fae9206628714fb2ce00f72e94f2258f"
|
||||
"shasum":"f572d396fae9206628714fb2ce00f72e94f2258f",
|
||||
"tarball":"http://localhost/npm/package.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"express":"^4.16.4"
|
||||
},
|
||||
"dist":{
|
||||
"shasum":"f572d396fae9206628714fb2ce00f72e94f2258f"
|
||||
"shasum":"f572d396fae9206628714fb2ce00f72e94f2258f",
|
||||
"tarball":"http://localhost/npm/package.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { merge } from 'lodash';
|
||||
|
||||
// This helper is for specs that use `gitlab/experimentation` module
|
||||
export function withGonExperiment(experimentKey, value = true) {
|
||||
let origGon;
|
||||
|
||||
|
@ -12,16 +13,26 @@ export function withGonExperiment(experimentKey, value = true) {
|
|||
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(() => {
|
||||
origGon = window.gon;
|
||||
window.gon = { experiment: { [experimentKey]: { variant } } };
|
||||
});
|
||||
// The following helper is for specs that use `gitlab-experiment` utilities,
|
||||
// which have a different schema that gets pushed to the frontend compared to
|
||||
// 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(() => {
|
||||
window.gon = origGon;
|
||||
Object.entries(experiments).forEach(([name, variant]) => {
|
||||
const experimentData = { experiment: name, variant };
|
||||
|
||||
// Deprecated
|
||||
window.gon.experiment[name] = experimentData;
|
||||
// Preferred
|
||||
window.gl.experiments[name] = experimentData;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import NewBoardButton from '~/boards/components/new_board_button.vue';
|
||||
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';
|
||||
|
||||
const FEATURE = 'prominent_create_board_btn';
|
||||
|
@ -28,7 +28,9 @@ describe('NewBoardButton', () => {
|
|||
});
|
||||
|
||||
describe('control variant', () => {
|
||||
assignGitlabExperiment(FEATURE, 'control');
|
||||
beforeAll(() => {
|
||||
stubExperiments({ [FEATURE]: 'control' });
|
||||
});
|
||||
|
||||
it('renders nothing', () => {
|
||||
wrapper = createComponent();
|
||||
|
@ -38,7 +40,9 @@ describe('NewBoardButton', () => {
|
|||
});
|
||||
|
||||
describe('candidate variant', () => {
|
||||
assignGitlabExperiment(FEATURE, 'candidate');
|
||||
beforeAll(() => {
|
||||
stubExperiments({ [FEATURE]: 'candidate' });
|
||||
});
|
||||
|
||||
it('renders New board button when `candidate` variant', () => {
|
||||
wrapper = createComponent();
|
||||
|
|
|
@ -241,18 +241,19 @@ describe('DiffFileHeader component', () => {
|
|||
});
|
||||
|
||||
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({
|
||||
props: {
|
||||
diffFile: {
|
||||
...diffFile,
|
||||
mode_changed: true,
|
||||
a_mode: 'old-mode',
|
||||
b_mode: 'new-mode',
|
||||
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/);
|
||||
});
|
||||
|
||||
it.each(otherModes.map((m) => [m]))(
|
||||
it.each(allModes.filter((m) => m[0] !== 'mode_changed'))(
|
||||
'for %s file mode does not display mode changes',
|
||||
(mode) => {
|
||||
createComponent({
|
||||
props: {
|
||||
diffFile: {
|
||||
...diffFile,
|
||||
mode_changed: false,
|
||||
a_mode: 'old-mode',
|
||||
b_mode: 'new-mode',
|
||||
viewer: {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { assignGitlabExperiment } from 'helpers/experimentation_helper';
|
||||
import { stubExperiments } from 'helpers/experimentation_helper';
|
||||
import {
|
||||
DEFAULT_VARIANT,
|
||||
CANDIDATE_VARIANT,
|
||||
|
@ -7,15 +7,45 @@ import {
|
|||
import * as experimentUtils from '~/experimentation/utils';
|
||||
|
||||
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', () => {
|
||||
const ABC_DATA = '_abc_data_';
|
||||
const ABC_DATA2 = '_updated_abc_data_';
|
||||
const DEF_DATA = '_def_data_';
|
||||
|
||||
describe.each`
|
||||
gon | input | output
|
||||
${[TEST_KEY, '_data_']} | ${[TEST_KEY]} | ${{ variant: '_data_' }}
|
||||
${[]} | ${[TEST_KEY]} | ${undefined}
|
||||
`('with input=$input and gon=$gon', ({ gon, input, output }) => {
|
||||
assignGitlabExperiment(...gon);
|
||||
gonData | glData | input | output
|
||||
${[ABC_KEY, ABC_DATA]} | ${[]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
|
||||
${[]} | ${[ABC_KEY, ABC_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
|
||||
${[ABC_KEY, ABC_DATA]} | ${[DEF_KEY, DEF_DATA]} | ${[ABC_KEY]} | ${{ experiment: ABC_KEY, variant: ABC_DATA }}
|
||||
${[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}`, () => {
|
||||
expect(experimentUtils.getExperimentData(...input)).toEqual(output);
|
||||
|
@ -25,66 +55,47 @@ describe('experiment Utilities', () => {
|
|||
|
||||
describe('getAllExperimentContexts', () => {
|
||||
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', () => {
|
||||
const experiments = [
|
||||
{ experiment: 'abc', variant: 'candidate' },
|
||||
{ experiment: 'def', variant: 'control' },
|
||||
{ experiment: 'ghi', variant: 'blue' },
|
||||
];
|
||||
window.gon = {
|
||||
experiment: experiments.reduce((collector, { experiment, variant }) => {
|
||||
return { ...collector, [experiment]: { experiment, variant } };
|
||||
}, {}),
|
||||
};
|
||||
const experiments = { [ABC_KEY]: 'candidate', [DEF_KEY]: 'control', ghi: 'blue' };
|
||||
|
||||
stubExperiments(experiments);
|
||||
|
||||
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', () => {
|
||||
window.gon.experiment = {};
|
||||
|
||||
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.each`
|
||||
gon | input | output
|
||||
${[TEST_KEY, DEFAULT_VARIANT]} | ${[TEST_KEY, DEFAULT_VARIANT]} | ${true}
|
||||
${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_variant_name']} | ${true}
|
||||
${[TEST_KEY, '_variant_name']} | ${[TEST_KEY, '_bogus_name']} | ${false}
|
||||
${[TEST_KEY, '_variant_name']} | ${['boguskey', '_variant_name']} | ${false}
|
||||
${[]} | ${[TEST_KEY, '_variant_name']} | ${false}
|
||||
`('with input=$input and gon=$gon', ({ gon, input, output }) => {
|
||||
assignGitlabExperiment(...gon);
|
||||
experiment | variant | input | output
|
||||
${ABC_KEY} | ${DEFAULT_VARIANT} | ${[ABC_KEY, DEFAULT_VARIANT]} | ${true}
|
||||
${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_variant_name']} | ${true}
|
||||
${ABC_KEY} | ${'_variant_name'} | ${[ABC_KEY, '_bogus_name']} | ${false}
|
||||
${ABC_KEY} | ${'_variant_name'} | ${['boguskey', '_variant_name']} | ${false}
|
||||
${undefined} | ${undefined} | ${[ABC_KEY, '_variant_name']} | ${false}
|
||||
`(
|
||||
'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', () => {
|
||||
const experiment = 'marley';
|
||||
const useSpy = jest.fn();
|
||||
const controlSpy = jest.fn();
|
||||
const trySpy = jest.fn();
|
||||
|
@ -98,49 +109,62 @@ describe('experiment Utilities', () => {
|
|||
};
|
||||
|
||||
describe('when there is no experiment data', () => {
|
||||
it('calls control variant', () => {
|
||||
experimentUtils.experiment('marley', variants);
|
||||
expect(useSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when experiment variant is "control"', () => {
|
||||
assignGitlabExperiment('marley', DEFAULT_VARIANT);
|
||||
|
||||
it('calls the control variant', () => {
|
||||
experimentUtils.experiment('marley', variants);
|
||||
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('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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when experiment variant is "candidate"', () => {
|
||||
assignGitlabExperiment('marley', CANDIDATE_VARIANT);
|
||||
beforeEach(() => {
|
||||
stubExperiments({ [experiment]: CANDIDATE_VARIANT });
|
||||
});
|
||||
|
||||
it('calls the candidate variant', () => {
|
||||
experimentUtils.experiment('marley', variants);
|
||||
it('calls the try variant', () => {
|
||||
experimentUtils.experiment(experiment, variants);
|
||||
expect(trySpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("when 'candidate' is provided instead of 'try'", () => {
|
||||
it('calls the control variant', () => {
|
||||
experimentUtils.experiment('marley', { candidate: candidateSpy });
|
||||
it('calls the candidate variant', () => {
|
||||
experimentUtils.experiment(experiment, { candidate: candidateSpy });
|
||||
expect(candidateSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
experimentUtils.experiment('marley', variants);
|
||||
experimentUtils.experiment(experiment, variants);
|
||||
expect(getUpStandUpSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@ -148,14 +172,17 @@ describe('experiment Utilities', () => {
|
|||
|
||||
describe('getExperimentVariant', () => {
|
||||
it.each`
|
||||
gon | input | output
|
||||
${{ experiment: { [TEST_KEY]: { variant: DEFAULT_VARIANT } } }} | ${[TEST_KEY]} | ${DEFAULT_VARIANT}
|
||||
${{ experiment: { [TEST_KEY]: { variant: CANDIDATE_VARIANT } } }} | ${[TEST_KEY]} | ${CANDIDATE_VARIANT}
|
||||
${{}} | ${[TEST_KEY]} | ${DEFAULT_VARIANT}
|
||||
`('with input=$input and gon=$gon, returns $output', ({ gon, input, output }) => {
|
||||
window.gon = gon;
|
||||
experiment | variant | input | output
|
||||
${ABC_KEY} | ${DEFAULT_VARIANT} | ${ABC_KEY} | ${DEFAULT_VARIANT}
|
||||
${ABC_KEY} | ${CANDIDATE_VARIANT} | ${ABC_KEY} | ${CANDIDATE_VARIANT}
|
||||
${undefined} | ${undefined} | ${ABC_KEY} | ${DEFAULT_VARIANT}
|
||||
`(
|
||||
'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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 JiraConnectApp from '~/jira_connect/subscriptions/components/app.vue';
|
||||
import AddNamespaceButton from '~/jira_connect/subscriptions/components/add_namespace_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 { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
|
||||
import { __ } from '~/locale';
|
||||
import { mockSubscription } from '../mock_data';
|
||||
|
||||
jest.mock('~/jira_connect/subscriptions/utils', () => ({
|
||||
retrieveAlert: jest.fn().mockReturnValue({ message: 'error message' }),
|
||||
|
@ -20,6 +22,8 @@ describe('JiraConnectApp', () => {
|
|||
const findAlertLink = () => findAlert().findComponent(GlLink);
|
||||
const findSignInButton = () => wrapper.findComponent(SignInButton);
|
||||
const findAddNamespaceButton = () => wrapper.findComponent(AddNamespaceButton);
|
||||
const findSubscriptionsList = () => wrapper.findComponent(SubscriptionsList);
|
||||
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||
|
||||
const createComponent = ({ provide, mountFn = shallowMount } = {}) => {
|
||||
store = createStore();
|
||||
|
@ -36,91 +40,114 @@ describe('JiraConnectApp', () => {
|
|||
|
||||
describe('template', () => {
|
||||
describe.each`
|
||||
scenario | usersPath | expectSignInButton | expectNamespaceButton
|
||||
${'user is not signed in'} | ${'/users'} | ${true} | ${false}
|
||||
${'user is signed in'} | ${undefined} | ${false} | ${true}
|
||||
`('when $scenario', ({ usersPath, expectSignInButton, expectNamespaceButton }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
provide: {
|
||||
usersPath,
|
||||
},
|
||||
scenario | usersPath | subscriptions | expectSignInButton | expectEmptyState | expectNamespaceButton | expectSubscriptionsList
|
||||
${'user is not signed in with subscriptions'} | ${'/users'} | ${[mockSubscription]} | ${true} | ${false} | ${false} | ${true}
|
||||
${'user is not signed in without subscriptions'} | ${'/users'} | ${undefined} | ${true} | ${false} | ${false} | ${false}
|
||||
${'user is signed in with subscriptions'} | ${undefined} | ${[mockSubscription]} | ${false} | ${false} | ${true} | ${true}
|
||||
${'user is signed in without subscriptions'} | ${undefined} | ${undefined} | ${false} | ${true} | ${false} | ${false}
|
||||
`(
|
||||
'when $scenario',
|
||||
({
|
||||
usersPath,
|
||||
expectSignInButton,
|
||||
subscriptions,
|
||||
expectEmptyState,
|
||||
expectNamespaceButton,
|
||||
expectSubscriptionsList,
|
||||
}) => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
provide: {
|
||||
usersPath,
|
||||
subscriptions,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders sign in button as expected', () => {
|
||||
expect(findSignInButton().exists()).toBe(expectSignInButton);
|
||||
});
|
||||
it(`${expectSignInButton ? 'renders' : 'does not render'} sign in button`, () => {
|
||||
expect(findSignInButton().exists()).toBe(expectSignInButton);
|
||||
});
|
||||
|
||||
it('renders "Add Namespace" button as expected', () => {
|
||||
expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton);
|
||||
});
|
||||
});
|
||||
it(`${expectEmptyState ? 'renders' : 'does not render'} empty state`, () => {
|
||||
expect(findEmptyState().exists()).toBe(expectEmptyState);
|
||||
});
|
||||
|
||||
describe('alert', () => {
|
||||
it.each`
|
||||
message | variant | alertShouldRender
|
||||
${'Test error'} | ${'danger'} | ${true}
|
||||
${'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();
|
||||
it(`${
|
||||
expectNamespaceButton ? 'renders' : 'does not render'
|
||||
} button to add namespace`, () => {
|
||||
expect(findAddNamespaceButton().exists()).toBe(expectNamespaceButton);
|
||||
});
|
||||
|
||||
store.commit(SET_ALERT, { message, variant });
|
||||
await wrapper.vm.$nextTick();
|
||||
it(`${expectSubscriptionsList ? 'renders' : 'does not render'} subscriptions list`, () => {
|
||||
expect(findSubscriptionsList().exists()).toBe(expectSubscriptionsList);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const alert = findAlert();
|
||||
|
||||
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 () => {
|
||||
describe('alert', () => {
|
||||
it.each`
|
||||
message | variant | alertShouldRender
|
||||
${'Test error'} | ${'danger'} | ${true}
|
||||
${'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: 'test message' });
|
||||
store.commit(SET_ALERT, { message, variant });
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
findAlert().vm.$emit('dismiss');
|
||||
await wrapper.vm.$nextTick();
|
||||
const alert = findAlert();
|
||||
|
||||
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 () => {
|
||||
createComponent({ mountFn: mount });
|
||||
const alertLink = findAlertLink();
|
||||
|
||||
store.commit(SET_ALERT, {
|
||||
message: __('test message %{linkStart}test link%{linkEnd}'),
|
||||
linkUrl: 'https://gitlab.com',
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(alertLink.exists()).toBe(true);
|
||||
expect(alertLink.text()).toContain('test link');
|
||||
expect(alertLink.attributes('href')).toBe('https://gitlab.com');
|
||||
});
|
||||
|
||||
const alertLink = findAlertLink();
|
||||
describe('when alert is set in localStoage', () => {
|
||||
it('renders alert on mount', () => {
|
||||
createComponent();
|
||||
|
||||
expect(alertLink.exists()).toBe(true);
|
||||
expect(alertLink.text()).toContain('test link');
|
||||
expect(alertLink.attributes('href')).toBe('https://gitlab.com');
|
||||
});
|
||||
const alert = findAlert();
|
||||
|
||||
describe('when alert is set in localStoage', () => {
|
||||
it('renders alert on mount', () => {
|
||||
createComponent();
|
||||
|
||||
const alert = findAlert();
|
||||
|
||||
expect(alert.exists()).toBe(true);
|
||||
expect(alert.html()).toContain('error message');
|
||||
});
|
||||
expect(alert.exists()).toBe(true);
|
||||
expect(alert.html()).toContain('error message');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
|
||||
import { mount, shallowMount } from '@vue/test-utils';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
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 createStore from '~/jira_connect/subscriptions/store';
|
||||
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
|
||||
import { reloadPage } from '~/jira_connect/subscriptions/utils';
|
||||
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||
import { mockSubscription } from '../mock_data';
|
||||
|
||||
jest.mock('~/jira_connect/subscriptions/utils');
|
||||
|
@ -15,11 +18,13 @@ describe('SubscriptionsList', () => {
|
|||
let wrapper;
|
||||
let store;
|
||||
|
||||
const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => {
|
||||
const createComponent = () => {
|
||||
store = createStore();
|
||||
|
||||
wrapper = mountFn(SubscriptionsList, {
|
||||
provide,
|
||||
wrapper = mount(SubscriptionsList, {
|
||||
provide: {
|
||||
subscriptions: [mockSubscription],
|
||||
},
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
@ -28,28 +33,28 @@ describe('SubscriptionsList', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||
const findGlTable = () => wrapper.findComponent(GlTable);
|
||||
const findUnlinkButton = () => findGlTable().findComponent(GlButton);
|
||||
const findUnlinkButton = () => wrapper.findComponent(GlButton);
|
||||
const clickUnlinkButton = () => findUnlinkButton().trigger('click');
|
||||
|
||||
describe('template', () => {
|
||||
it('renders GlEmptyState when subscriptions is empty', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
|
||||
expect(findGlEmptyState().exists()).toBe(true);
|
||||
expect(findGlTable().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders GlTable when subscriptions are present', () => {
|
||||
createComponent({
|
||||
provide: {
|
||||
subscriptions: [mockSubscription],
|
||||
},
|
||||
});
|
||||
it('renders "name" cell correctly', () => {
|
||||
const groupItemNames = wrapper.findAllComponents(GroupItemName);
|
||||
expect(groupItemNames.wrappers).toHaveLength(1);
|
||||
|
||||
expect(findGlEmptyState().exists()).toBe(false);
|
||||
expect(findGlTable().exists()).toBe(true);
|
||||
const item = groupItemNames.at(0);
|
||||
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;
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
mountFn: mount,
|
||||
provide: {
|
||||
subscriptions: [mockSubscription],
|
||||
},
|
||||
});
|
||||
createComponent();
|
||||
removeSubscriptionSpy = jest.spyOn(JiraConnectApi, 'removeSubscription').mockResolvedValue();
|
||||
});
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ describe('Wip', () => {
|
|||
is_new_mr_data: true,
|
||||
};
|
||||
|
||||
describe('handleRemoveWIP', () => {
|
||||
describe('handleRemoveDraft', () => {
|
||||
it('should make a request to service and handle response', (done) => {
|
||||
const vm = createComponent();
|
||||
|
||||
|
@ -59,7 +59,7 @@ describe('Wip', () => {
|
|||
}),
|
||||
);
|
||||
|
||||
vm.handleRemoveWIP();
|
||||
vm.handleRemoveDraft();
|
||||
setImmediate(() => {
|
||||
expect(vm.isMakingRequest).toBeTruthy();
|
||||
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.querySelector('button').getAttribute('disabled')).toBeTruthy();
|
||||
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',
|
||||
);
|
||||
});
|
||||
|
@ -93,7 +93,7 @@ describe('Wip', () => {
|
|||
vm.mr.removeWIPPath = '';
|
||||
|
||||
Vue.nextTick(() => {
|
||||
expect(el.querySelector('.js-remove-wip')).toEqual(null);
|
||||
expect(el.querySelector('.js-remove-draft')).toEqual(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,7 +15,7 @@ describe('getStateKey', () => {
|
|||
branchMissing: false,
|
||||
commitsCount: 2,
|
||||
hasConflicts: false,
|
||||
workInProgress: false,
|
||||
draft: false,
|
||||
};
|
||||
const bound = getStateKey.bind(context);
|
||||
|
||||
|
@ -49,9 +49,9 @@ describe('getStateKey', () => {
|
|||
|
||||
expect(bound()).toEqual('unresolvedDiscussions');
|
||||
|
||||
context.workInProgress = true;
|
||||
context.draft = true;
|
||||
|
||||
expect(bound()).toEqual('workInProgress');
|
||||
expect(bound()).toEqual('draft');
|
||||
|
||||
context.onlyAllowMergeIfPipelineSucceeds = true;
|
||||
context.isPipelineFailed = true;
|
||||
|
@ -99,7 +99,7 @@ describe('getStateKey', () => {
|
|||
branchMissing: false,
|
||||
commitsCount: 2,
|
||||
hasConflicts: false,
|
||||
workInProgress: false,
|
||||
draft: false,
|
||||
};
|
||||
const bound = getStateKey.bind(context);
|
||||
|
||||
|
|
|
@ -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
|
|
@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
|
|||
notes discussions user_permissions id iid title title_html description
|
||||
description_html state created_at updated_at source_project target_project
|
||||
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
|
||||
diff_refs diff_stats diff_stats_summary
|
||||
force_remove_source_branch
|
||||
|
|
|
@ -3,14 +3,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
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
|
||||
expect(described_class).to have_graphql_mutation(Mutations::MergeRequests::SetDraft)
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue