Add latest changes from gitlab-org/gitlab@master

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

View File

@ -105,5 +105,16 @@ In this rollout issue, ensure the scoped `experiment::` label is kept accurate.
/chatops run feature set <experiment-key> false
```
## 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"

View File

@ -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) {

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

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

View File

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

View File

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

View File

@ -17,8 +17,8 @@ export default function deviseState() {
return stateKey.rebase;
} 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) {

View File

@ -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();

View File

@ -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',

View File

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

View File

@ -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

View File

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

View File

@ -53,9 +53,6 @@ module Types
description: 'Indicates if the source branch is protected.'
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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

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

View File

@ -39,6 +39,7 @@ class Packages::Package < ApplicationRecord
has_one :nuget_metadatum, inverse_of: :package, class_name: 'Packages::Nuget::Metadatum'
has_one :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) }

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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.

View File

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

View File

@ -23,7 +23,7 @@
= form_tag personal_access_token_import_github_path, method: :post do
.form-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

View File

@ -9,20 +9,9 @@
= link_to _('Sign in to GitLab'), jira_connect_users_path, target: '_blank', rel: 'noopener noreferrer', class: 'js-jira-connect-sign-in'
%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/' }

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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)

View File

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

View File

@ -1,8 +1,8 @@
---
name: rate_limiter_safe_increment
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

View File

@ -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

View File

@ -591,6 +591,9 @@ Settings.cron_jobs['batched_background_migrations_worker']['job_class'] = 'Datab
Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances'] ||= Settingslogic.new({})
Settings.cron_jobs['issues_reschedule_stuck_issue_rebalances']['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({})

View File

@ -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

View File

@ -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.

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
50a5c8af2cde1ae79d627f70d3b266488f76f76b481aefca8516db5360cfa843

View File

@ -0,0 +1 @@
13cf3d164d541df48b6d14d7cc1953113476ba8ea5975d7d0c5f84098e2e0e61

View File

@ -0,0 +1 @@
a48f62bed7e4c4a0e69acd3b340065317aff71602e696970276a4e443f1dcabf

View File

@ -0,0 +1 @@
a2556a3d8b21e59caa6cbf7f83d621fef391904d0c13c77c0e5da713a580b4c9

View File

@ -13237,7 +13237,8 @@ CREATE TABLE dependency_proxy_blobs (
file_store integer,
file_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;

View File

@ -3509,31 +3509,6 @@ Input type: `MergeRequestSetSubscriptionInput`
| <a id="mutationmergerequestsetsubscriptionerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -208,6 +208,15 @@ Then, you can run `npm publish` either locally or by using GitLab CI/CD.
- **GitLab CI/CD:** Set an `NPM_TOKEN` [CI/CD variable](../../../ci/variables/index.md)
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

View File

@ -4,31 +4,40 @@ group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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?

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -62,7 +62,7 @@ RSpec.describe Dashboard::TodosController do
create(:issue, project: project, assignees: [user])
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)

View File

@ -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

View File

@ -10,7 +10,8 @@ RSpec.describe Projects::MergeRequestsController do
let_it_be_with_reload(:project_public_with_private_builds) { create(:project, :repository, :public, :builds_private) }
let(: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

View File

@ -499,13 +499,12 @@ RSpec.describe RegistrationsController do
expect(User.last.name).to eq("#{base_user_params[:first_name]} #{base_user_params[:last_name]}")
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

View File

@ -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 }

View File

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

View File

@ -115,6 +115,19 @@ RSpec.describe 'GPG signed commits' do
end
end
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

View File

@ -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
}
}

View File

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

View File

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

View File

@ -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;
});
}

View File

@ -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();

View File

@ -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: {

View File

@ -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);
},
);
});
});

View File

@ -1,12 +1,14 @@
import { GlAlert, GlLink } from '@gitlab/ui';
import { GlAlert, GlLink, GlEmptyState } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import 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');
});
});
});

View File

@ -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();
});

View File

@ -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();
});
});

View File

@ -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);

View File

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

View File

@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
notes discussions user_permissions id iid title title_html description
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

View File

@ -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