Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-03-15 12:13:34 +00:00
parent 2cd5f04547
commit b50c9d31e3
101 changed files with 1698 additions and 367 deletions

View File

@ -685,37 +685,31 @@ rspec-ee unit pg12 opensearch1:
extends:
- .rspec-ee-base-pg12-opensearch1
- .rspec-ee-unit-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee unit pg12 opensearch2:
extends:
- .rspec-ee-base-pg12-opensearch2
- .rspec-ee-unit-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee integration pg12 opensearch1:
extends:
- .rspec-ee-base-pg12-opensearch1
- .rspec-ee-integration-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee integration pg12 opensearch2:
extends:
- .rspec-ee-base-pg12-opensearch2
- .rspec-ee-integration-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee system pg12 opensearch1:
extends:
- .rspec-ee-base-pg12-opensearch1
- .rspec-ee-system-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
rspec-ee system pg12 opensearch2:
extends:
- .rspec-ee-base-pg12-opensearch2
- .rspec-ee-system-parallel
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
# PG13
rspec-ee migration pg13:

View File

@ -620,6 +620,7 @@
.rails:rules:run-search-tests:
rules:
- !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-ee-only", rules]
- <<: *if-merge-request-labels-group-global-search
changes: *search-backend-patterns
- <<: *if-merge-request-labels-group-global-search

View File

@ -1 +1 @@
8eb2d65be9607663316e0939d2550fa22df98ea0
6d7521c579ecb1b0a76c4c29e12ce9b72dd4057e

View File

@ -19,9 +19,13 @@ export default {
return {
initialFilterValue: [],
initialSortBy: DEFAULT_SORT,
tokens: [...FILTERED_SEARCH_TOKENS, buildFilteredSearchCategoryToken(this.categories)],
};
},
computed: {
tokens() {
return [...FILTERED_SEARCH_TOKENS, buildFilteredSearchCategoryToken(this.categories)];
},
},
created() {
const query = queryToObject(window.location.search);

View File

@ -23,6 +23,12 @@ export const FILTERED_SEARCH_TOKEN_USER = {
defaultUsers: [],
};
export const FILTERED_SEARCH_TOKEN_REPORTER = {
...FILTERED_SEARCH_TOKEN_USER,
type: 'reporter',
title: __('Reporter'),
};
export const FILTERED_SEARCH_TOKEN_STATUS = {
type: 'status',
icon: 'status',
@ -68,4 +74,8 @@ export const FILTERED_SEARCH_TOKEN_CATEGORY = {
operators: OPERATORS_IS,
};
export const FILTERED_SEARCH_TOKENS = [FILTERED_SEARCH_TOKEN_USER, FILTERED_SEARCH_TOKEN_STATUS];
export const FILTERED_SEARCH_TOKENS = [
FILTERED_SEARCH_TOKEN_USER,
FILTERED_SEARCH_TOKEN_REPORTER,
FILTERED_SEARCH_TOKEN_STATUS,
];

View File

@ -1,6 +1,6 @@
import { renderHtmlStreams } from '~/streaming/render_html_streams';
import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
import { toPolyfillReadable } from '~/streaming/polyfills';

View File

@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { getParameterByName, updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';

View File

@ -1,3 +1,4 @@
import { KeyMod, KeyCode } from 'monaco-editor';
import { getModifierKey } from '~/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__, __, sprintf } from '~/locale';
@ -73,7 +74,8 @@ export const EXTENSION_MARKDOWN_BUTTONS = [
}),
data: {
mdTag: '**',
mdShortcuts: '["mod+b"]',
// eslint-disable-next-line no-bitwise
mdShortcuts: [KeyMod.CtrlCmd | KeyCode.KeyB],
},
},
{
@ -83,7 +85,8 @@ export const EXTENSION_MARKDOWN_BUTTONS = [
}),
data: {
mdTag: '_',
mdShortcuts: '["mod+i"]',
// eslint-disable-next-line no-bitwise
mdShortcuts: [KeyMod.CtrlCmd | KeyCode.KeyI],
},
},
{
@ -93,7 +96,8 @@ export const EXTENSION_MARKDOWN_BUTTONS = [
}),
data: {
mdTag: '~~',
mdShortcuts: '["mod+shift+x]',
// eslint-disable-next-line no-bitwise
mdShortcuts: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyX],
},
},
{
@ -120,7 +124,8 @@ export const EXTENSION_MARKDOWN_BUTTONS = [
data: {
mdTag: '[{text}](url)',
mdSelect: 'url',
mdShortcuts: '["mod+k"]',
// eslint-disable-next-line no-bitwise
mdShortcuts: [KeyMod.CtrlCmd | KeyCode.KeyK],
},
},
{

View File

@ -8,6 +8,7 @@ export class EditorMarkdownExtension {
onSetup(instance) {
this.toolbarButtons = [];
this.actions = [];
if (instance.toolbar) {
this.setupToolbar(instance);
}
@ -17,10 +18,26 @@ export class EditorMarkdownExtension {
if (instance.toolbar) {
instance.toolbar.removeItems(ids);
}
this.actions.forEach((action) => {
action.dispose();
});
this.actions = [];
}
setupToolbar(instance) {
this.toolbarButtons = EXTENSION_MARKDOWN_BUTTONS.map((btn) => {
if (btn.data.mdShortcuts) {
this.actions.push(
instance.addAction({
id: btn.id,
label: btn.label,
keybindings: btn.data.mdShortcuts,
run(inst) {
inst.insertMarkdown(btn.data);
},
}),
);
}
return {
...btn,
icon: btn.id,
@ -66,12 +83,8 @@ export class EditorMarkdownExtension {
instance.setPosition(pos);
},
insertMarkdown: (instance, e) => {
const {
mdTag: tag,
mdBlock: blockTag,
mdPrepend,
mdSelect: select,
} = e.currentTarget.dataset;
const { mdTag: tag, mdBlock: blockTag, mdPrepend, mdSelect: select } =
e.currentTarget?.dataset || e;
insertMarkdownText({
tag,

View File

@ -0,0 +1,101 @@
<script>
import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { getAgentLastContact, getAgentStatus } from '~/clusters_list/clusters_util';
import { TOKEN_STATUS_ACTIVE } from '~/clusters/agents/constants';
import { AGENT_STATUSES } from '~/clusters_list/constants';
import { s__ } from '~/locale';
import getK8sClusterAgentQuery from '../graphql/queries/k8s_cluster_agent.query.graphql';
export default {
components: {
GlIcon,
GlLink,
GlSprintf,
GlLoadingIcon,
TimeAgoTooltip,
GlAlert,
},
props: {
agentName: {
required: true,
type: String,
},
agentId: {
required: true,
type: String,
},
agentProjectPath: {
required: true,
type: String,
},
},
apollo: {
clusterAgent: {
query: getK8sClusterAgentQuery,
variables() {
return {
agentName: this.agentName,
projectPath: this.agentProjectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
};
},
update: (data) => data?.project?.clusterAgent,
error() {
this.clusterAgent = null;
},
},
},
data() {
return {
clusterAgent: null,
};
},
computed: {
isLoading() {
return this.$apollo.queries.clusterAgent.loading;
},
agentLastContact() {
return getAgentLastContact(this.clusterAgent.tokens.nodes);
},
agentStatus() {
return getAgentStatus(this.agentLastContact);
},
},
methods: {},
i18n: {
loadingError: s__('ClusterAgents|An error occurred while loading your agent'),
agentId: s__('ClusterAgents|Agent ID #%{agentId}'),
neverConnectedText: s__('ClusterAgents|Never'),
},
AGENT_STATUSES,
};
</script>
<template>
<gl-loading-icon v-if="isLoading" inline />
<div v-else-if="clusterAgent" class="gl-text-gray-900">
<gl-icon name="kubernetes-agent" class="gl-text-gray-500" />
<gl-link :href="clusterAgent.webPath" class="gl-mr-3">
<gl-sprintf :message="$options.i18n.agentId"
><template #agentId>{{ agentId }}</template></gl-sprintf
>
</gl-link>
<span class="gl-mr-3" data-testid="agent-status">
<gl-icon
:name="$options.AGENT_STATUSES[agentStatus].icon"
:class="$options.AGENT_STATUSES[agentStatus].class"
/>
{{ $options.AGENT_STATUSES[agentStatus].name }}
</span>
<span data-testid="agent-last-used-date">
<gl-icon name="calendar" />
<time-ago-tooltip v-if="agentLastContact" :time="agentLastContact" />
<span v-else>{{ $options.i18n.neverConnectedText }}</span>
</span>
</div>
<gl-alert v-else variant="danger" :dismissible="false">
{{ $options.i18n.loadingError }}
</gl-alert>
</template>

View File

@ -0,0 +1,73 @@
<script>
import { GlCollapse, GlButton } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import KubernetesAgentInfo from './kubernetes_agent_info.vue';
export default {
components: {
GlCollapse,
GlButton,
KubernetesAgentInfo,
},
props: {
agentName: {
required: true,
type: String,
},
agentId: {
required: true,
type: String,
},
agentProjectPath: {
required: true,
type: String,
},
},
data() {
return {
isVisible: false,
};
},
computed: {
chevronIcon() {
return this.isVisible ? 'chevron-down' : 'chevron-right';
},
label() {
return this.isVisible ? this.$options.i18n.collapse : this.$options.i18n.expand;
},
},
methods: {
toggleCollapse() {
this.isVisible = !this.isVisible;
},
},
i18n: {
collapse: __('Collapse'),
expand: __('Expand'),
sectionTitle: s__('Environment|Kubernetes overview'),
},
};
</script>
<template>
<div class="gl-px-4">
<p class="gl-font-weight-bold gl-text-gray-500 gl-display-flex gl-mb-0">
<gl-button
:icon="chevronIcon"
:aria-label="label"
category="tertiary"
size="small"
class="gl-mr-3"
@click="toggleCollapse"
/>{{ $options.i18n.sectionTitle }}
</p>
<gl-collapse :visible="isVisible" class="gl-md-pl-7 gl-md-pr-5 gl-mt-4">
<template v-if="isVisible">
<kubernetes-agent-info
:agent-name="agentName"
:agent-id="agentId"
:agent-project-path="agentProjectPath"
class="gl-mb-5"
/></template>
</gl-collapse>
</div>
</template>

View File

@ -11,6 +11,7 @@ import {
import { __, s__ } from '~/locale';
import { truncate } from '~/lib/utils/text_utility';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql';
import ExternalUrl from './environment_external_url.vue';
import Actions from './environment_actions.vue';
@ -22,6 +23,7 @@ import Terminal from './environment_terminal_button.vue';
import Delete from './environment_delete.vue';
import Deployment from './deployment.vue';
import DeployBoardWrapper from './deploy_board_wrapper.vue';
import KubernetesOverview from './kubernetes_overview.vue';
export default {
components: {
@ -42,6 +44,7 @@ export default {
Terminal,
TimeAgoTooltip,
Delete,
KubernetesOverview,
EnvironmentAlert: () => import('ee_component/environments/components/environment_alert.vue'),
EnvironmentApproval: () =>
import('ee_component/environments/components/environment_approval.vue'),
@ -49,6 +52,7 @@ export default {
directives: {
GlTooltip,
},
mixins: [glFeatureFlagsMixin()],
inject: ['helpPagePath'],
props: {
environment: {
@ -162,6 +166,18 @@ export default {
rolloutStatus() {
return this.environment?.rolloutStatus;
},
agent() {
return this.environment?.agent || {};
},
isKubernetesOverviewAvailable() {
return this.glFeatures?.kasUserAccessProject;
},
hasRequiredAgentData() {
return this.agent.project && this.agent.id && this.agent.name;
},
showKubernetesOverview() {
return this.isKubernetesOverviewAvailable && this.hasRequiredAgentData;
},
},
methods: {
toggleCollapse() {
@ -184,6 +200,13 @@ export default {
'gl-md-pl-7',
'gl-bg-gray-10',
],
kubernetesOverviewClasses: [
'gl-border-gray-100',
'gl-border-t-solid',
'gl-border-1',
'gl-py-4',
'gl-bg-gray-10',
],
};
</script>
<template>
@ -340,6 +363,13 @@ export default {
</template>
</gl-sprintf>
</div>
<div v-if="showKubernetesOverview" :class="$options.kubernetesOverviewClasses">
<kubernetes-overview
:agent-project-path="agent.project"
:agent-name="agent.name"
:agent-id="agent.id"
/>
</div>
<div v-if="rolloutStatus" :class="$options.deployBoardClasses">
<deploy-board-wrapper
:rollout-status="rolloutStatus"

View File

@ -0,0 +1,19 @@
query getK8sClusterAgentQuery(
$projectPath: ID!
$agentName: String!
$tokenStatus: AgentTokenStatus!
) {
project(fullPath: $projectPath) {
id
clusterAgent(name: $agentName) {
id
webPath
tokens(status: $tokenStatus) {
nodes {
id
lastUsedAt
}
}
}
}
}

View File

@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_USERS_SAVED_REPLY } from '~/graphql_shared/constants';

View File

@ -25,7 +25,7 @@
}
}
.gd {
.gi {
background-color: var(--diff-addition-color);
}
}

View File

@ -71,6 +71,11 @@ class Import::GiteaController < Import::GithubController
end
end
override :serialized_imported_projects
def serialized_imported_projects(projects = already_added_projects)
ProjectSerializer.new.represent(projects, serializer: :import, provider_url: provider_url)
end
override :client_repos
def client_repos
@client_repos ||= filtered(client.repos)

View File

@ -145,7 +145,10 @@ class Import::GithubController < Import::BaseController
end
def serialized_imported_projects(projects = already_added_projects)
ProjectSerializer.new.represent(projects, serializer: :import, provider_url: provider_url)
ProjectSerializer.new.represent(
projects,
serializer: :import, provider_url: provider_url, client: client_proxy
)
end
def expire_etag_cache

View File

@ -20,6 +20,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:environment_details_vue, @project)
end
before_action only: [:index] do
push_frontend_feature_flag(:kas_user_access_project, @project)
end
before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect]
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]

View File

@ -9,9 +9,9 @@ module BulkImports
def config_for(portable)
case portable
when ::Project
FileTransfer::ProjectConfig.new(portable)
::BulkImports::FileTransfer::ProjectConfig.new(portable)
when ::Group
FileTransfer::GroupConfig.new(portable)
::BulkImports::FileTransfer::GroupConfig.new(portable)
else
raise(UnsupportedObjectType, "Unsupported object type: #{portable.class}")
end

View File

@ -7,21 +7,28 @@ module Ci
# CI/CD Catalog given a namespace as a scope.
# This model is not directly backed by a table and joins catalog resources
# with projects to return relevant data.
def initialize(namespace)
def initialize(namespace, current_user)
raise ArgumentError, 'Namespace is not a root namespace' unless namespace.root?
@namespace = namespace
@current_user = current_user
end
def resources
Ci::Catalog::Resource
.joins(:project).includes(:project)
.merge(Project.in_namespace(namespace.self_and_descendant_ids))
.merge(projects_in_namespace_visible_to_user)
end
private
attr_reader :namespace
attr_reader :namespace, :current_user
def projects_in_namespace_visible_to_user
Project
.in_namespace(namespace.self_and_descendant_ids)
.public_or_visible_to_user(current_user)
end
end
end
end

View File

@ -5,7 +5,7 @@
# after a period of time (10 minutes).
# When an attribute is incremented by a value, the increment is added
# to a Redis key. Then, FlushCounterIncrementsWorker will execute
# `flush_increments_to_database!` which removes increments from Redis for a
# `commit_increment!` which removes increments from Redis for a
# given model attribute and updates the values in the database.
#
# @example:
@ -29,8 +29,24 @@
# counter_attribute :conditional_one, if: -> { |object| object.use_counter_attribute? }
# end
#
# The `counter_attribute` by default will return last persisted value.
# It's possible to always return accurate (real) value instead by using `returns_current: true`.
# While doing this the `counter_attribute` will overwrite attribute accessor to fetch
# the buffered information added to the last persisted value. This will incur cost a Redis call per attribute fetched.
#
# @example:
#
# class ProjectStatistics
# include CounterAttribute
#
# counter_attribute :commit_count, returns_current: true
# end
#
# in that case
# model.commit_count => persisted value + buffered amount to be added
#
# To increment the counter we can use the method:
# increment_counter(:commit_count, 3)
# increment_amount(:commit_count, 3)
#
# This method would determine whether it would increment the counter using Redis,
# or fallback to legacy increment on ActiveRecord counters.
@ -50,11 +66,22 @@ module CounterAttribute
include Gitlab::Utils::StrongMemoize
class_methods do
def counter_attribute(attribute, if: nil)
def counter_attribute(attribute, if: nil, returns_current: false)
counter_attributes << {
attribute: attribute,
if_proc: binding.local_variable_get(:if) # can't read `if` directly
if_proc: binding.local_variable_get(:if), # can't read `if` directly
returns_current: returns_current
}
if returns_current
define_method(attribute) do
current_counter(attribute)
end
end
define_method("increment_#{attribute}") do |amount|
increment_amount(attribute, amount)
end
end
def counter_attributes
@ -87,6 +114,15 @@ module CounterAttribute
end
end
def increment_amount(attribute, amount)
counter = Gitlab::Counters::Increment.new(amount: amount)
increment_counter(attribute, counter)
end
def current_counter(attribute)
read_attribute(attribute) + counter(attribute).get
end
def increment_counter(attribute, increment)
return if increment.amount == 0
@ -172,7 +208,8 @@ module CounterAttribute
Gitlab::AppLogger.info(
message: 'Acquiring lease for project statistics update',
project_statistics_id: id,
model: self.class.name,
model_id: id,
project_id: project.id,
**log_fields,
**Gitlab::ApplicationContext.current
@ -184,7 +221,8 @@ module CounterAttribute
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
Gitlab::AppLogger.warn(
message: 'Concurrent project statistics update detected',
project_statistics_id: id,
model: self.class.name,
model_id: id,
project_id: project.id,
**log_fields,
**Gitlab::ApplicationContext.current

View File

@ -8,11 +8,17 @@ module WebHooks
class_methods do
def auto_disabling_enabled?
ENABLED_HOOK_TYPES.include?(name) &&
enabled_hook_types.include?(name) &&
Gitlab::SafeRequestStore.fetch(:auto_disabling_web_hooks) do
Feature.enabled?(:auto_disabling_web_hooks, type: :ops)
end
end
private
def enabled_hook_types
ENABLED_HOOK_TYPES
end
end
included do
@ -135,3 +141,6 @@ module WebHooks
end
end
end
WebHooks::AutoDisabling.prepend_mod
WebHooks::AutoDisabling::ClassMethods.prepend_mod

View File

@ -2832,7 +2832,7 @@ class Project < ApplicationRecord
end
def all_protected_branches
if Feature.enabled?(:group_protected_branches)
if Feature.enabled?(:group_protected_branches, group)
@all_protected_branches ||= ProtectedBranch.from_union([protected_branches, group_protected_branches])
else
protected_branches

View File

@ -4,6 +4,9 @@
# This class ensures that we keep 1 record per project per month.
module Projects
class DataTransfer < ApplicationRecord
include AfterCommitQueue
include CounterAttribute
self.table_name = 'project_data_transfers'
belongs_to :project
@ -11,6 +14,11 @@ module Projects
scope :current_month, -> { where(date: beginning_of_month) }
counter_attribute :repository_egress, returns_current: true
counter_attribute :artifacts_egress, returns_current: true
counter_attribute :packages_egress, returns_current: true
counter_attribute :registry_egress, returns_current: true
def self.beginning_of_month(time = Time.current)
time.utc.beginning_of_month
end

View File

@ -43,7 +43,7 @@ class ProtectedBranch < ApplicationRecord
end
def self.allow_force_push?(project, ref_name)
if Feature.enabled?(:group_protected_branches)
if Feature.enabled?(:group_protected_branches, project.group)
protected_branches = project.all_protected_branches.matching(ref_name)
project_protected_branches, group_protected_branches = protected_branches.partition(&:project_id)
@ -67,11 +67,7 @@ class ProtectedBranch < ApplicationRecord
end
def self.protected_refs(project)
if Feature.enabled?(:group_protected_branches)
project.all_protected_branches
else
project.protected_branches
end
project.all_protected_branches
end
# overridden in EE

View File

@ -48,7 +48,7 @@ class Repository
# For example, for entry `:commit_count` there's a method called `commit_count` which
# stores its data in the `commit_count` cache key.
CACHED_METHODS = %i(size commit_count readme_path contribution_guide
changelog license_blob license_licensee license_gitaly gitignore
changelog license_blob license_gitaly gitignore
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? root_ref merged_branch_names
has_visible_content? issue_template_names_hash merge_request_template_names_hash
@ -60,7 +60,7 @@ class Repository
METHOD_CACHES_FOR_FILE_TYPES = {
readme: %i(readme_path),
changelog: :changelog,
license: %i(license_blob license_licensee license_gitaly),
license: %i(license_blob license_gitaly),
contributing: :contribution_guide,
gitignore: :gitignore,
gitlab_ci: :gitlab_ci_yml,
@ -656,24 +656,13 @@ class Repository
end
def license
if Feature.enabled?(:license_from_gitaly)
license_gitaly
else
license_licensee
end
license_gitaly
end
def license_licensee
return unless exists?
raw_repository.license(false)
end
cache_method :license_licensee
def license_gitaly
return unless exists?
raw_repository.license(true)
raw_repository.license
end
cache_method :license_gitaly

View File

@ -16,4 +16,11 @@ class ProjectImportEntity < ProjectEntity
expose :import_error, if: ->(project) { project.import_state&.failed? } do |project|
project.import_failures.last&.exception_message
end
# Only for GitHub importer where we pass client through
expose :relation_type do |project, options|
next nil if options[:client].nil? || Feature.disabled?(:remove_legacy_github_client)
::Gitlab::GithubImport::ProjectRelationType.new(options[:client]).for(project.import_source)
end
end

View File

@ -45,11 +45,7 @@ module Projects
end
def protected_branch_exists?
if Feature.enabled?(:group_protected_branches)
project.all_protected_branches.find_by_name(default_branch).present?
else
project.protected_branches.find_by_name(default_branch).present?
end
project.all_protected_branches.find_by_name(default_branch).present?
end
def default_branch

View File

@ -73,7 +73,8 @@ module ProtectedBranches
end
def redis_key
@redis_key ||= if Feature.enabled?(:group_protected_branches)
group = project_or_group.is_a?(Group) ? project_or_group : project_or_group.group
@redis_key ||= if Feature.enabled?(:group_protected_branches, group)
[CACHE_ROOT_KEY, project_or_group.class.name, project_or_group.id].join(':')
else
[CACHE_ROOT_KEY, project_or_group.id].join(':')

View File

@ -16,7 +16,8 @@
= image_tag @appearance.header_logo_path, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
= link_to _('Remove header logo'), header_logos_admin_application_settings_appearances_path, data: { confirm: _("Header logo will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove header logo') }, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm"
= render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: header_logos_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Header logo will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove header logo') } }) do
= _('Remove header logo')
%hr
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: "", accept: 'image/*'
@ -35,7 +36,8 @@
= image_tag @appearance.favicon_path, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
= link_to _('Remove favicon'), favicon_admin_application_settings_appearances_path, data: { confirm: _("Favicon will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove favicon') }, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm"
= render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: favicon_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Favicon will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove favicon') } }) do
= _('Remove favicon')
%hr
= f.hidden_field :favicon_cache
= f.file_field :favicon, class: '', accept: 'image/*'
@ -67,7 +69,8 @@
= image_tag @appearance.logo_path, class: 'appearance-logo-preview'
- if @appearance.persisted?
%br
= link_to _('Remove logo'), logo_admin_application_settings_appearances_path, data: { confirm: _("Logo will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove logo') }, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm remove-logo"
= render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: logo_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Logo will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove logo') } }) do
= _('Remove logo')
%hr
= f.hidden_field :logo_cache
= f.file_field :logo, class: "", accept: 'image/*'
@ -98,7 +101,8 @@
= image_tag @appearance.pwa_icon_path, class: 'appearance-pwa-icon-preview'
- if @appearance.persisted?
%br
= link_to _('Remove icon'), pwa_icon_admin_application_settings_appearances_path, data: { confirm: _("Icon will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove icon') }, method: :delete, class: "btn gl-button btn-danger btn-danger-secondary btn-sm remove-logo"
= render Pajamas::ButtonComponent.new(variant: :danger, category: :secondary, size: :small, method: :delete, href: pwa_icon_admin_application_settings_appearances_path, button_options: { data: { confirm: _("Icon will be removed. Are you sure?"), confirm_btn_variant: "danger" }, aria: { label: _('Remove icon') } }) do
= _('Remove icon')
%hr
= f.hidden_field :pwa_icon_cache
= f.file_field :pwa_icon, class: "", accept: 'image/*'

View File

@ -17,6 +17,9 @@
= s_('Preferences|Color theme')
%p
= s_('Preferences|Customize the color of GitLab.')
- if show_super_sidebar?
%p
= s_('Preferences|Note: You have the new navigation enabled, so only Dark Mode theme significantly changes GitLab\'s appearance.')
.col-lg-8.application-theme
.row
- Gitlab::Themes.each do |theme|

View File

@ -1,8 +0,0 @@
---
name: license_from_gitaly
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77041
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/374300
milestone: '15.5'
type: development
group: group::gitaly
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: limited_capacity_seat_refresh_worker_high
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104099
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725
milestone: '15.9'
type: development
group: group::utilization
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: limited_capacity_seat_refresh_worker_low
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104099
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725
milestone: '15.9'
type: development
group: group::utilization
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: limited_capacity_seat_refresh_worker_medium
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104099
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382725
milestone: '15.9'
type: development
group: group::utilization
default_enabled: false

View File

@ -0,0 +1,50 @@
const { parse, compile: compilerDomCompile } = require('@vue/compiler-dom');
const getPropIndex = (node, prop) => node.props?.findIndex((p) => p.name === prop) ?? -1;
function modifyKeysInsideTemplateTag(templateNode) {
let keyCandidate = null;
for (const node of templateNode.children) {
const keyBindingIndex = node.props
? node.props.findIndex((prop) => prop.arg && prop.arg.content === 'key')
: -1;
if (keyBindingIndex !== -1 && getPropIndex(node, 'for') === -1) {
if (!keyCandidate) {
keyCandidate = node.props[keyBindingIndex];
}
node.props.splice(keyBindingIndex, 1);
}
}
if (keyCandidate) {
templateNode.props.push(keyCandidate);
}
}
module.exports = {
parse,
compile(template, options) {
const rootNode = parse(template, options);
const pendingNodes = [rootNode];
while (pendingNodes.length) {
const currentNode = pendingNodes.pop();
if (getPropIndex(currentNode, 'for') !== -1) {
if (currentNode.tag === 'template') {
// This one will be dropped all together with compiler when we drop Vue.js 2 support
modifyKeysInsideTemplateTag(currentNode);
}
// This one will be dropped when https://github.com/vuejs/core/issues/7725 will be fixed
const vOncePropIndex = getPropIndex(currentNode, 'once');
if (vOncePropIndex !== -1) {
currentNode.props.splice(vOncePropIndex, 1);
}
}
currentNode.children?.forEach((child) => pendingNodes.push(child));
}
return compilerDomCompile(rootNode, options);
},
};

View File

@ -195,9 +195,6 @@ const alias = {
ROOT_PATH,
'app/assets/javascripts/sentry/sentry_browser_wrapper.js',
),
// temporary alias until we replace all `flash` imports for `alert`
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109449
'~/flash': path.join(ROOT_PATH, 'app/assets/javascripts/alert.js'),
'~': path.join(ROOT_PATH, 'app/assets/javascripts'),
emojis: path.join(ROOT_PATH, 'fixtures/emojis'),
empty_states: path.join(ROOT_PATH, 'app/views/shared/empty_states'),
@ -301,12 +298,15 @@ if (WEBPACK_USE_ESBUILD_LOADER) {
}
const vueLoaderOptions = {
ident: 'vue-loader-options',
cacheDirectory: path.join(CACHE_PATH, 'vue-loader'),
cacheIdentifier: [
process.env.NODE_ENV || 'development',
webpack.version,
VUE_VERSION,
VUE_LOADER_VERSION,
EXPLICIT_VUE_VERSION,
].join('|'),
};
@ -334,6 +334,8 @@ if (USE_VUE3) {
Object.assign(alias, {
vue: '@vue/compat',
});
vueLoaderOptions.compiler = require.resolve('./vue3migration/compiler');
}
module.exports = {

View File

@ -19,13 +19,13 @@
#
- title: "Trigger jobs can mirror downstream pipeline status exactly" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
announcement_milestone: "15.9" # (required) The milestone when this feature was first announced as deprecated.
removal_milestone: "16.0" # (required) The milestone when this feature is planned to be removed
removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
breaking_change: true # (required) Change to false if this is not a breaking change.
reporter: dhershkovitch # (required) GitLab username of the person reporting the change
stage: verify # (required) String value of the stage that the feature was created in. e.g., Growth
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285493 # (required) Link to the deprecation issue in GitLab
body: | # (required) Do not modify this line, instead modify the lines below.
In some cases, like when a downstream pipeline had the `passed with warnings` status, trigger jobs that were using [`strategy: depend`](https://docs.gitlab.com/ee/ci/yaml/index.html#strategydepend) did not mirror the status of the downstream pipeline exactly. In GitLab 16.0 trigger jobs will show the exact same status as the the downstream pipeline. If your pipeline relied on this behavior, you should update your pipeline to handle the more accurate status.
In some cases, like when a downstream pipeline had the `passed with warnings` status, trigger jobs that were using [`strategy: depend`](https://docs.gitlab.com/ee/ci/yaml/index.html#strategydepend) did not mirror the status of the downstream pipeline exactly. In GitLab 17.0 trigger jobs will show the exact same status as the the downstream pipeline. If your pipeline relied on this behavior, you should update your pipeline to handle the more accurate status.
#
# OPTIONAL END OF SUPPORT FIELDS
#

View File

@ -253,13 +253,16 @@ Example response:
## Project Audit Events
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219238) in GitLab 13.1.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219238) in GitLab 13.1.
> - [Support for keyset pagination added](https://gitlab.com/gitlab-org/gitlab/-/issues/367528) in GitLab 15.10.
The Project Audit Events API allows you to retrieve [project audit events](../administration/audit_events.md#project-events).
A user with a Maintainer role (or above) can retrieve project audit events of all users.
A user with a Developer role is limited to project audit events based on their individual actions.
When requesting consecutive pages of results, you should use [keyset pagination](rest/index.md#keyset-based-pagination).
### Retrieve all project audit events
```plaintext

View File

@ -483,12 +483,13 @@ pagination headers.
Keyset-based pagination is supported only for selected resources and ordering
options:
| Resource | Options | Availability |
|:---------------------------------------------------------|:---------------------------------|:-------------------------------------------------------------------------------------------------------------|
| [Projects](../projects.md) | `order_by=id` only | Authenticated and unauthenticated users |
| [Groups](../groups.md) | `order_by=name`, `sort=asc` only | Unauthenticated users only |
| [Group audit events](../audit_events.md#group-audit-events) | `order_by=id`, `sort=desc` only | Authenticated users only ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/333968) in GitLab 15.2) |
| [Jobs](../jobs.md) | `order_by=id`, `sort=desc` only | Authenticated users only ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362172) in GitLab 15.9) |
| Resource | Options | Availability |
|:----------------------------------------------------------------|:---------------------------------|:--------------------------------------------------------------------------------------------------------------|
| [Projects](../projects.md) | `order_by=id` only | Authenticated and unauthenticated users |
| [Groups](../groups.md) | `order_by=name`, `sort=asc` only | Unauthenticated users only |
| [Group audit events](../audit_events.md#group-audit-events) | `order_by=id`, `sort=desc` only | Authenticated users only ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/333968) in GitLab 15.2) |
| [Project audit events](../audit_events.md#project-audit-events) | `order_by=id`, `sort=desc` only | Authenticated users only ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367528) in GitLab 15.10) |
| [Jobs](../jobs.md) | `order_by=id`, `sort=desc` only | Authenticated users only ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362172) in GitLab 15.9) |
### Pagination response headers

View File

@ -45,7 +45,7 @@ The following fields are included in each ID token:
| [`iss`](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.1) | Always | Issuer of the token, which is the domain of the GitLab instance ("issuer" claim). |
| [`jti`](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.7) | Always | Unique identifier for the token ("JWT ID" claim). |
| [`nbf`](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.5) | Always | The time after which the token becomes valid ("not before" claim). |
| [`sub`](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2) | Always | The job ID ("subject" claim). |
| [`sub`](https://www.rfc-editor.org/rfc/rfc7519.html#section-4.1.2) | Always | `project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}` ("subject" claim). |
| `deployment_tier` | Job specifies an environment | [Deployment tier](../environments/index.md#deployment-tier-of-environments) of the environment the job specifies. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/363590) in GitLab 15.2. |
| `environment_protected` | Job specifies an environment | `true` if specified environment is protected, `false` otherwise. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/294440) in GitLab 13.9. |
| `environment` | Job specifies an environment | Environment the job specifies. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/294440) in GitLab 13.9. |

View File

@ -6,13 +6,42 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Contribute to the GitLab documentation
Everyone is welcome to update the GitLab documentation.
Everyone is welcome to update the GitLab documentation!
If you are looking for something to work on, you can review the list of
[open issues](https://about.gitlab.com/handbook/product/ux/technical-writing/#community-contribution-opportunities)
or work to fix [Vale](testing.md#vale) suggestions.
## Work without an issue
You don't need an issue to update the documentation, however.
You don't need an issue to update the documentation.
On [https://docs.gitlab.com](https://docs.gitlab.com), at the bottom of any page,
you can select **View page source** or **Edit in Web IDE** and [get started with a merge request](#open-your-merge-request).
You can alternately:
- Choose a page [in the `/doc` directory](https://gitlab.com/gitlab-org/gitlab/-/tree/master/doc)
and edit it from there.
- Try installing and running the [Vale linting tool](testing.md#vale)
and fixing the resulting issues.
When you're developing code, the workflow for updating docs is slightly different.
For details, see the [merge request workflow](../contributing/merge_request_workflow.md).
## Search available issues
If you're looking for an open issue, you can
[review the list of documentation issues curated specifically for new contributors](https://gitlab.com/gitlab-org/gitlab/-/issues/?sort=created_date&state=opened&label_name%5B%5D=documentation&label_name%5B%5D=docs-only&label_name%5B%5D=Seeking%20community%20contributions&first_page_size=20).
When you find an issue you'd like to work on:
- If the issue is already assigned to someone, pick a different one.
- If the issue is unassigned, add a comment and ask to work on the issue. For a Hackathon, use `@docs-hackathon`. Otherwise, use `@gl-docsteam`. For example:
```plaintext
@docs-hackathon I would like to work on this issue
```
- Do not ask for more than three issues at a time.
## Open your merge request
When you are ready to update the documentation:

View File

@ -64,7 +64,7 @@ Instead, you should obtain an instance of the `ContentEditor` class by listening
```html
<script>
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
export default {

View File

@ -90,7 +90,7 @@ In this file, we write the actions that call mutations for handling a list of us
```javascript
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
export const fetchUsers = ({ state, dispatch }) => {
commit(types.REQUEST_USERS);

View File

@ -758,17 +758,17 @@ We will be transitioning to a new IID as a result of moving requirements to a [w
</div>
<div class="deprecation removal-160 breaking-change">
<div class="deprecation removal-170 breaking-change">
### Trigger jobs can mirror downstream pipeline status exactly
Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span>
Planned removal: GitLab <span class="removal-milestone">17.0</span> <span class="removal-date"></span>
WARNING:
This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
Review the details carefully before upgrading.
In some cases, like when a downstream pipeline had the `passed with warnings` status, trigger jobs that were using [`strategy: depend`](https://docs.gitlab.com/ee/ci/yaml/index.html#strategydepend) did not mirror the status of the downstream pipeline exactly. In GitLab 16.0 trigger jobs will show the exact same status as the the downstream pipeline. If your pipeline relied on this behavior, you should update your pipeline to handle the more accurate status.
In some cases, like when a downstream pipeline had the `passed with warnings` status, trigger jobs that were using [`strategy: depend`](https://docs.gitlab.com/ee/ci/yaml/index.html#strategydepend) did not mirror the status of the downstream pipeline exactly. In GitLab 17.0 trigger jobs will show the exact same status as the the downstream pipeline. If your pipeline relied on this behavior, you should update your pipeline to handle the more accurate status.
</div>
</div>

View File

@ -198,8 +198,8 @@ example, milestones have been created and CI for testing and setting environment
Value stream analytics records the following times for each stage:
- **Issue**: 09:00 to 11:00: 2 hrs
- **Plan**: 11:00 to 12:00: 1 hr
- **Code**: 12:00 to 14:00: 2 hrs
- **Plan**: 11:00 to 12:30: 1.5 hr
- **Code**: 12:30 to 14:00: 1.5 hrs
- **Test**: 5 minutes
- **Review**: 14:00 to 19:00: 5 hrs
- **Staging**: 19:00 to 19:30: 30 minutes

View File

@ -222,6 +222,8 @@ Remove or deactivate a user on the identity provider to remove their access to:
After the identity provider performs a sync based on its configured schedule, the user's membership is revoked and they
lose access.
When you enable SCIM, this does not automatically remove existing users who do not have a SAML identity.
NOTE:
Deprovisioning does not delete the GitLab user account.

View File

@ -120,9 +120,9 @@ Endpoints should follow these best practices:
## Failing webhooks
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60837) in GitLab 13.12 [with a flag](../../../administration/feature_flags.md) named `web_hooks_disable_failed`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/329849) in GitLab 15.7.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/329849) in GitLab 15.7. Feature flag `web_hooks_disable_failed` removed.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60837) for project webhooks in GitLab 13.12 [with a flag](../../../administration/feature_flags.md) named `web_hooks_disable_failed`. Disabled by default.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/329849) for project webhooks in GitLab 15.7. Feature flag `web_hooks_disable_failed` removed.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/385902) for group webhooks in GitLab 15.10.
> - [Disabled on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/390157) in GitLab 15.10 [with a flag](../../../administration/feature_flags.md) named `auto_disabling_web_hooks`.
FLAG:

View File

@ -44,6 +44,7 @@ module.exports = (path, options = {}) => {
Object.assign(globals, {
'vue-jest': {
experimentalCSSCompile: false,
compiler: require.resolve('./config/vue3migration/compiler'),
compilerOptions: {
compatConfig: {
MODE: 2,
@ -85,9 +86,6 @@ module.exports = (path, options = {}) => {
const TEST_FIXTURES_PATTERN = 'test_fixtures(/.*)$';
const moduleNameMapper = {
// temporary alias until we replace all `flash` imports for `alert`
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109449
'^~/flash$': '<rootDir>/app/assets/javascripts/alert',
'^~(/.*)\\?(worker|raw)$': '<rootDir>/app/assets/javascripts$1',
'^(.*)\\?(worker|raw)$': '$1',
'^~(/.*)$': '<rootDir>/app/assets/javascripts$1',

View File

@ -16,8 +16,6 @@ module BulkImports
return unless relation_hash
relation_definition = import_export_config.top_relation_tree(relation)
relation_object = deep_transform_relation!(relation_hash, relation, relation_definition) do |key, hash|
relation_factory.create(
relation_index: relation_index,
@ -35,8 +33,29 @@ module BulkImports
relation_object
end
def load(_, object)
object&.save!
def load(_context, object)
return unless object
if object.new_record?
saver = Gitlab::ImportExport::Base::RelationObjectSaver.new(
relation_object: object,
relation_key: relation,
relation_definition: relation_definition,
importable: portable
)
saver.execute
capture_invalid_subrelations(saver.invalid_subrelations)
else
if object.invalid?
Gitlab::Import::Errors.merge_nested_errors(object)
raise(ActiveRecord::RecordInvalid, object)
end
object.save!
end
end
def deep_transform_relation!(relation_hash, relation_key, relation_definition, &block)
@ -104,6 +123,22 @@ module BulkImports
def portable_class_sym
portable.class.to_s.downcase.to_sym
end
def relation_definition
import_export_config.top_relation_tree(relation)
end
def capture_invalid_subrelations(invalid_subrelations)
invalid_subrelations.each do |record|
BulkImports::Failure.create(
bulk_import_entity_id: tracker.entity.id,
pipeline_class: tracker.pipeline_name,
exception_class: 'RecordInvalid',
exception_message: record.errors.full_messages.to_sentence.truncate(255),
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id
)
end
end
end
end
end

View File

@ -70,7 +70,11 @@ module Gitlab
def project
return legacy_project if ::Feature.disabled?(:ci_batch_project_includes_context, context.project)
BatchLoader.for(project_name).batch do |project_names, loader|
# Although we use `where_full_path_in`, this BatchLoader does not reduce the number of queries to 1.
# That's because we use it in the `can_access_local_content?` and `sha` BatchLoaders
# as the `for` parameter. And this loads the project immediately.
BatchLoader.for(project_name)
.batch do |project_names, loader|
::Project.where_full_path_in(project_names.uniq).each do |project|
# We are using the same downcase in the `initialize` method.
loader.call(project.full_path.downcase, project)
@ -83,13 +87,11 @@ module Gitlab
return legacy_can_access_local_content?
end
context.logger.instrument(:config_file_project_validate_access) do
BatchLoader.for(project_name)
.batch(key: context.user) do |project_names, loader, args|
project_names.uniq.each do |project_name|
context.logger.instrument(:config_file_project_validate_access) do
loader.call(project_name, Ability.allowed?(args[:key], :download_code, project))
end
BatchLoader.for(project)
.batch(key: context.user) do |projects, loader, args|
projects.uniq.each do |project|
context.logger.instrument(:config_file_project_validate_access) do
loader.call(project, Ability.allowed?(args[:key], :download_code, project))
end
end
end
@ -98,9 +100,10 @@ module Gitlab
def sha
return legacy_sha if ::Feature.disabled?(:ci_batch_project_includes_context, context.project)
BatchLoader.for([project_name, ref_name]).batch do |project_name_ref_pairs, loader|
project_name_ref_pairs.uniq.each do |project_name, ref_name|
loader.call([project_name, ref_name], project.commit(ref_name).try(:sha))
BatchLoader.for([project, ref_name])
.batch do |project_ref_pairs, loader|
project_ref_pairs.uniq.each do |project, ref_name|
loader.call([project, ref_name], project.commit(ref_name).try(:sha))
end
end
end

View File

@ -803,27 +803,17 @@ module Gitlab
end
end
def license(from_gitaly)
def license
wrapped_gitaly_errors do
response = gitaly_repository_client.find_license
break nil if response.license_short_name.empty?
if from_gitaly
break ::Gitlab::Git::DeclaredLicense.new(key: response.license_short_name,
name: response.license_name,
nickname: response.license_nickname.presence,
url: response.license_url.presence,
path: response.license_path)
end
licensee_object = Licensee::License.new(response.license_short_name)
break nil if licensee_object.name.blank?
licensee_object.meta.nickname = "LICENSE" if licensee_object.key == "other"
licensee_object
::Gitlab::Git::DeclaredLicense.new(key: response.license_short_name,
name: response.license_name,
nickname: response.license_nickname.presence,
url: response.license_url.presence,
path: response.license_path)
end
rescue Licensee::InvalidLicense => e
Gitlab::ErrorTracking.track_exception(e)

View File

@ -6,6 +6,8 @@ module Gitlab
class Proxy
attr_reader :client
delegate :each_object, :user, :octokit, to: :client
def initialize(access_token, client_options)
@client = pick_client(access_token, client_options)
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
class ProjectRelationType
CACHE_ORGS_EXPIRES_IN = 5.minutes
CACHE_USER_EXPIRES_IN = 1.hour
def initialize(client)
@client = client
end
def for(import_source)
namespace = import_source.split('/')[0]
if user?(namespace)
'owned'
elsif organization?(namespace)
'organization'
else
'collaborated'
end
end
private
attr_reader :client
def user?(namespace)
github_user_login == namespace
end
def organization?(namespace)
github_org_logins.include? namespace
end
def github_user_login
::Rails.cache.fetch(cache_key('user_login'), expire_in: CACHE_USER_EXPIRES_IN) do
client.user(nil)[:login]
end
end
def github_org_logins
::Rails.cache.fetch(cache_key('organization_logins'), expires_in: CACHE_ORGS_EXPIRES_IN) do
logins = []
client.each_object(:organizations) { |org| logins.push(org[:login]) }
logins
end
end
def cache_key(subject)
['github_import', Gitlab::CryptoHelper.sha256(client.octokit.access_token), subject].join('/')
end
end
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Gitlab
module Import
module Errors
# Merges all nested subrelation errors into base errors object.
#
# @example
# issue = Project.last.issues.new(
# title: 'test',
# author: User.first,
# notes: [Note.new(
# award_emoji: [AwardEmoji.new(name: 'test')]
# )])
#
# issue.validate
# issue.errors.full_messages
# => ["Notes is invalid"]
#
# Gitlab::Import::Errors.merge_nested_errors(issue)
# issue.errors.full_messages
# => ["Notes is invalid",
# "Award emoji is invalid",
# "Awardable can't be blank",
# "Name is not a valid emoji name",
# ...
# ]
def self.merge_nested_errors(object)
object.errors.each do |error|
association = object.class.reflect_on_association(error.attribute)
next unless association&.collection?
records = object.public_send(error.attribute).select(&:invalid?) # rubocop: disable GitlabSecurity/PublicSend
records.each do |record|
merge_nested_errors(record)
object.errors.merge!(record.errors)
end
end
end
end
end
end

View File

@ -17,6 +17,8 @@ module Gitlab
BATCH_SIZE = 100
MIN_RECORDS_SIZE = 1
attr_reader :invalid_subrelations
# @param relation_object [Object] Object of a project/group, e.g. an issue
# @param relation_key [String] Name of the object association to group/project, e.g. :issues
# @param relation_definition [Hash] Object subrelations as defined in import_export.yml
@ -43,14 +45,11 @@ module Gitlab
relation_object.save!
save_subrelations
ensure
log_invalid_subrelations
end
private
attr_reader :relation_object, :relation_key, :relation_definition,
:importable, :collection_subrelations, :invalid_subrelations
attr_reader :relation_object, :relation_key, :relation_definition, :importable, :collection_subrelations
# rubocop:disable GitlabSecurity/PublicSend
def save_subrelations
@ -92,30 +91,6 @@ module Gitlab
end
end
# rubocop:enable GitlabSecurity/PublicSend
def log_invalid_subrelations
invalid_subrelations.flatten.each do |record|
Gitlab::Import::Logger.info(
message: '[Project/Group Import] Invalid subrelation',
importable_column_name => importable.id,
relation_key: relation_key,
error_messages: record.errors.full_messages.to_sentence
)
ImportFailure.create(
source: 'RelationObjectSaver#save!',
relation_key: relation_key,
exception_class: 'RecordInvalid',
exception_message: record.errors.full_messages.to_sentence,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id,
importable_column_name => importable.id
)
end
end
def importable_column_name
@column_name ||= importable.class.reflect_on_association(:import_failures).foreign_key.to_sym
end
end
end
end

View File

@ -90,13 +90,23 @@ module Gitlab
def save_relation_object(relation_object, relation_key, relation_definition, relation_index)
if relation_object.new_record?
Gitlab::ImportExport::Base::RelationObjectSaver.new(
saver = Gitlab::ImportExport::Base::RelationObjectSaver.new(
relation_object: relation_object,
relation_key: relation_key,
relation_definition: relation_definition,
importable: @importable
).execute
)
saver.execute
log_invalid_subrelations(saver.invalid_subrelations, relation_key)
else
if relation_object.invalid?
Gitlab::Import::Errors.merge_nested_errors(relation_object)
raise(ActiveRecord::RecordInvalid, relation_object)
end
import_failure_service.with_retry(action: 'relation_object.save!', relation_key: relation_key, relation_index: relation_index) do
relation_object.save!
end
@ -290,6 +300,32 @@ module Gitlab
message: '[Project/Group Import] Created new object relation'
)
end
def log_invalid_subrelations(invalid_subrelations, relation_key)
invalid_subrelations.flatten.each do |record|
Gitlab::Import::Errors.merge_nested_errors(record)
@shared.logger.info(
message: '[Project/Group Import] Invalid subrelation',
importable_column_name => @importable.id,
relation_key: relation_key,
error_messages: record.errors.full_messages.to_sentence
)
::ImportFailure.create(
source: 'RelationTreeRestorer#save_relation_object',
relation_key: relation_key,
exception_class: 'ActiveRecord::RecordInvalid',
exception_message: record.errors.full_messages.to_sentence,
correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id,
importable_column_name => @importable.id
)
end
end
def importable_column_name
@column_name ||= @importable.class.reflect_on_association(:import_failures).foreign_key.to_sym
end
end
end
end

View File

@ -34,6 +34,7 @@ module Gitlab
}
)
end
alias_method :octokit, :api
def client
unless config

View File

@ -9537,6 +9537,9 @@ msgstr ""
msgid "ClusterAgents|Agent %{strongStart}disconnected%{strongEnd}"
msgstr ""
msgid "ClusterAgents|Agent ID #%{agentId}"
msgstr ""
msgid "ClusterAgents|Agent access token:"
msgstr ""
@ -16350,6 +16353,9 @@ msgstr ""
msgid "Environment|Deployment tier"
msgstr ""
msgid "Environment|Kubernetes overview"
msgstr ""
msgid "Epic"
msgstr ""
@ -32658,6 +32664,9 @@ msgstr ""
msgid "Preferences|Must be a number between %{min} and %{max}"
msgstr ""
msgid "Preferences|Note: You have the new navigation enabled, so only Dark Mode theme significantly changes GitLab's appearance."
msgstr ""
msgid "Preferences|Opt out of the Web IDE Beta"
msgstr ""

View File

@ -0,0 +1,22 @@
diff --git a/node_modules/@vue/vue3-jest/lib/process.js b/node_modules/@vue/vue3-jest/lib/process.js
index a8d1c5c..a6b2036 100644
--- a/node_modules/@vue/vue3-jest/lib/process.js
+++ b/node_modules/@vue/vue3-jest/lib/process.js
@@ -108,12 +108,17 @@ function processTemplate(descriptor, filename, config) {
(descriptor.script && descriptor.script.lang)
const isTS = /^typescript$|tsx?$/.test(lang)
+ const compiler = typeof vueJestConfig.compiler === 'string'
+ ? require(vueJestConfig.compiler)
+ : vueJestConfig.compiler
+
const result = compileTemplate({
id: filename,
source: template.content,
filename,
preprocessLang: template.lang,
preprocessOptions: vueJestConfig[template.lang],
+ compiler,
compilerOptions: {
bindingMetadata: bindings,
mode: 'module',

View File

@ -0,0 +1,154 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Verify', :runner, product_group: :pipeline_authoring, feature_flag: {
name: 'ci_batch_project_includes_context',
scope: :global
} do
describe 'Include multiple files from multiple projects' do
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" }
let(:main_project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-pipeline'
end
end
let(:project1) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'external-project-1'
end
end
let(:project2) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'external-project-2'
end
end
let!(:runner) do
Resource::ProjectRunner.fabricate! do |runner|
runner.project = main_project
runner.name = executor
runner.tags = [executor]
end
end
def before_do
Flow::Login.sign_in
add_included_files_for(main_project)
add_included_files_for(project1)
add_included_files_for(project2)
add_main_ci_file(main_project)
main_project.visit!
Flow::Pipeline.visit_latest_pipeline(status: 'passed')
end
after do
runner.remove_via_api!
end
context 'when FF is on', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/396374' do
before do
Runtime::Feature.enable(:ci_batch_project_includes_context, project: main_project)
sleep 60
before_do
end
it 'runs the pipeline with composed config' do
Page::Project::Pipeline::Show.perform do |pipeline|
aggregate_failures 'pipeline has all expected jobs' do
expect(pipeline).to have_job('test_for_main')
expect(pipeline).to have_job("test1_for_#{project1.full_path}")
expect(pipeline).to have_job("test1_for_#{project2.full_path}")
expect(pipeline).to have_job("test2_for_#{project1.full_path}")
expect(pipeline).to have_job("test2_for_#{main_project.full_path}")
end
end
end
end
context 'when FF is off', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/396375' do
before do
Runtime::Feature.disable(:ci_batch_project_includes_context, project: main_project)
sleep 60
before_do
end
it 'runs the pipeline with composed config' do
Page::Project::Pipeline::Show.perform do |pipeline|
aggregate_failures 'pipeline has all expected jobs' do
expect(pipeline).to have_job('test_for_main')
expect(pipeline).to have_job("test1_for_#{project1.full_path}")
expect(pipeline).to have_job("test1_for_#{project2.full_path}")
expect(pipeline).to have_job("test2_for_#{project1.full_path}")
expect(pipeline).to have_job("test2_for_#{main_project.full_path}")
end
end
end
end
private
def add_included_files_for(project)
files = [
{
file_path: 'file1.yml',
content: <<~YAML
test1_for_#{project.full_path}:
tags: ["#{executor}"]
script: echo hello1
YAML
},
{
file_path: 'file2.yml',
content: <<~YAML
test2_for_#{project.full_path}:
tags: ["#{executor}"]
script: echo hello2
YAML
}
]
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add files'
commit.add_files(files)
end
end
def add_main_ci_file(project)
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add config file'
commit.add_files([main_ci_file])
end
end
def main_ci_file
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
include:
- project: #{project1.full_path}
file: file1.yml
- project: #{project2.full_path}
file: file1.yml
- project: #{project1.full_path}
file: file2.yml
- project: #{main_project.full_path}
file: file2.yml
test_for_main:
tags: ["#{executor}"]
script: echo hello
YAML
}
end
end
end
end

View File

@ -169,6 +169,9 @@ RSpec.describe Import::GithubController, feature_category: :importers do
if client_auth_success
allow(proxy).to receive(:repos).and_return({ repos: provider_repos })
allow(proxy).to receive(:client).and_return(client_stub)
allow_next_instance_of(Gitlab::GithubImport::ProjectRelationType) do |instance|
allow(instance).to receive(:for).with('example/repo').and_return('owned')
end
else
allow(proxy).to receive(:repos).and_raise(Octokit::Unauthorized)
end
@ -347,7 +350,13 @@ RSpec.describe Import::GithubController, feature_category: :importers do
end
end
describe "POST create" do
describe "POST create", :clean_gitlab_redis_cache do
before do
allow_next_instance_of(Gitlab::GithubImport::ProjectRelationType) do |instance|
allow(instance).to receive(:for).with("#{provider_username}/vim").and_return('owned')
end
end
it_behaves_like 'a GitHub-ish import controller: POST create'
it_behaves_like 'project import rate limiter'
@ -376,13 +385,22 @@ RSpec.describe Import::GithubController, feature_category: :importers do
end
describe "POST cancel" do
let_it_be(:project) { create(:project, :import_started, import_type: 'github', import_url: 'https://fake.url') }
let_it_be(:project) do
create(
:project, :import_started,
import_type: 'github', import_url: 'https://fake.url', import_source: 'login/repo'
)
end
context 'when project import was canceled' do
before do
allow(Import::Github::CancelProjectImportService)
.to receive(:new).with(project, user)
.and_return(double(execute: { status: :success, project: project }))
allow_next_instance_of(Gitlab::GithubImport::ProjectRelationType) do |instance|
allow(instance).to receive(:for).with('login/repo').and_return('owned')
end
end
it 'returns success' do

View File

@ -222,7 +222,7 @@ FactoryBot.define do
# the transient `files` attribute. Each file will be created in its own
# commit, operating against the master branch. So, the following call:
#
# create(:project, :custom_repo, files: { 'foo/a.txt' => 'foo', 'b.txt' => bar' })
# create(:project, :custom_repo, files: { 'foo/a.txt' => 'foo', 'b.txt' => 'bar' })
#
# will create a repository containing two files, and two commits, in master
trait :custom_repo do
@ -245,6 +245,19 @@ FactoryBot.define do
end
end
# A basic repository with a single file 'test.txt'. It also has the HEAD as the default branch.
trait :small_repo do
custom_repo
files { { 'test.txt' => 'test' } }
after(:create) do |project|
Sidekiq::Worker.skipping_transaction_check do
raise "Failed to assign the repository head!" unless project.change_head(project.default_branch_or_main)
end
end
end
# Test repository - https://gitlab.com/gitlab-org/gitlab-test
trait :repository do
test_repo

View File

@ -5,6 +5,7 @@ import AbuseReportsFilteredSearchBar from '~/admin/abuse_reports/components/abus
import {
FILTERED_SEARCH_TOKENS,
FILTERED_SEARCH_TOKEN_USER,
FILTERED_SEARCH_TOKEN_REPORTER,
FILTERED_SEARCH_TOKEN_STATUS,
FILTERED_SEARCH_TOKEN_CATEGORY,
DEFAULT_SORT,
@ -74,7 +75,7 @@ describe('AbuseReportsFilteredSearchBar', () => {
});
it('parses and passes search param to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
setWindowLocation('?status=closed&user=mr_abuser');
setWindowLocation('?status=closed&user=mr_abuser&reporter=ms_nitch');
createComponent();
@ -83,6 +84,10 @@ describe('AbuseReportsFilteredSearchBar', () => {
type: FILTERED_SEARCH_TOKEN_USER.type,
value: { data: 'mr_abuser', operator: '=' },
},
{
type: FILTERED_SEARCH_TOKEN_REPORTER.type,
value: { data: 'ms_nitch', operator: '=' },
},
{
type: FILTERED_SEARCH_TOKEN_STATUS.type,
value: { data: 'closed', operator: '=' },
@ -121,6 +126,10 @@ describe('AbuseReportsFilteredSearchBar', () => {
type: FILTERED_SEARCH_TOKEN_USER.type,
value: { data: 'mr_abuser', operator: '=' },
};
const REPORTER_FILTER_TOKEN = {
type: FILTERED_SEARCH_TOKEN_REPORTER.type,
value: { data: 'ms_nitch', operator: '=' },
};
const STATUS_FILTER_TOKEN = {
type: FILTERED_SEARCH_TOKEN_STATUS.type,
value: { data: 'open', operator: '=' },
@ -140,7 +149,7 @@ describe('AbuseReportsFilteredSearchBar', () => {
findFilteredSearchBar().vm.$emit('onFilter', filterTokens);
};
it.each([USER_FILTER_TOKEN, STATUS_FILTER_TOKEN, CATEGORY_FILTER_TOKEN])(
it.each([USER_FILTER_TOKEN, REPORTER_FILTER_TOKEN, STATUS_FILTER_TOKEN, CATEGORY_FILTER_TOKEN])(
'redirects with $type query param',
(filterToken) => {
createComponentAndFilter([filterToken]);

View File

@ -9,10 +9,10 @@ import { VSA_METRICS_GROUPS, METRICS_POPOVER_CONTENT } from '~/analytics/shared/
import { prepareTimeMetricsData } from '~/analytics/shared/utils';
import MetricTile from '~/analytics/shared/components/metric_tile.vue';
import ValueStreamsDashboardLink from '~/analytics/shared/components/value_streams_dashboard_link.vue';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { group } from './mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
describe('ValueStreamMetrics', () => {
let wrapper;

View File

@ -5,14 +5,14 @@ import { renderHtmlStreams } from '~/streaming/render_html_streams';
import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
import { toPolyfillReadable } from '~/streaming/polyfills';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
jest.mock('~/streaming/render_html_streams');
jest.mock('~/streaming/rate_limit_stream_requests');
jest.mock('~/streaming/handle_streamed_anchor_link');
jest.mock('~/streaming/polyfills');
jest.mock('~/sentry');
jest.mock('~/flash');
jest.mock('~/alert');
global.fetch = jest.fn();

View File

@ -17,6 +17,7 @@ describe('Markdown Extension for Source Editor', () => {
const thirdLine = 'string with some **markup**';
const text = `${firstLine}\n${secondLine}\n${thirdLine}`;
const markdownPath = 'foo.md';
let extensions;
const setSelection = (startLineNumber = 1, startColumn = 1, endLineNumber = 1, endColumn = 1) => {
const selection = new Range(startLineNumber, startColumn, endLineNumber, endColumn);
@ -38,7 +39,10 @@ describe('Markdown Extension for Source Editor', () => {
blobPath: markdownPath,
blobContent: text,
});
instance.use([{ definition: ToolbarExtension }, { definition: EditorMarkdownExtension }]);
extensions = instance.use([
{ definition: ToolbarExtension },
{ definition: EditorMarkdownExtension },
]);
});
afterEach(() => {
@ -59,6 +63,25 @@ describe('Markdown Extension for Source Editor', () => {
});
});
describe('markdown keystrokes', () => {
it('registers all keystrokes as actions', () => {
EXTENSION_MARKDOWN_BUTTONS.forEach((button) => {
if (button.data.mdShortcuts) {
expect(instance.getAction(button.id)).toBeDefined();
}
});
});
it('disposes all keystrokes on unuse', () => {
instance.unuse(extensions[1]);
EXTENSION_MARKDOWN_BUTTONS.forEach((button) => {
if (button.data.mdShortcuts) {
expect(instance.getAction(button.id)).toBeNull();
}
});
});
});
describe('getSelectedText', () => {
it('does not fail if there is no selection and returns the empty string', () => {
jest.spyOn(instance, 'getSelection');

View File

@ -12,14 +12,14 @@ import {
} from '~/editor/constants';
import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext';
import SourceEditor from '~/editor/source_editor';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import syntaxHighlight from '~/syntax_highlight';
import { spyOnApi } from './helpers';
jest.mock('~/syntax_highlight');
jest.mock('~/flash');
jest.mock('~/alert');
describe('Markdown Live Preview Extension for Source Editor', () => {
let editor;

View File

@ -35,7 +35,7 @@ describe('~/environments/components/environments_folder.vue', () => {
...propsData,
},
stubs: { transition: stubTransition() },
provide: { helpPagePath: '/help' },
provide: { helpPagePath: '/help', projectId: '1' },
});
beforeEach(async () => {

View File

@ -798,3 +798,9 @@ export const resolvedDeploymentDetails = {
},
},
};
export const agent = {
project: 'agent-project',
id: '1',
name: 'agent-name',
};

View File

@ -0,0 +1,126 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlIcon, GlLink, GlSprintf, GlLoadingIcon, GlAlert } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
import { TOKEN_STATUS_ACTIVE } from '~/clusters/agents/constants';
import { AGENT_STATUSES, ACTIVE_CONNECTION_TIME } from '~/clusters_list/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getK8sClusterAgentQuery from '~/environments/graphql/queries/k8s_cluster_agent.query.graphql';
Vue.use(VueApollo);
const propsData = {
agentName: 'my-agent',
agentId: '1',
agentProjectPath: 'path/to/agent-config-project',
};
const mockClusterAgent = {
id: '1',
name: 'token-1',
webPath: 'path/to/agent-page',
};
const connectedTimeNow = new Date();
const connectedTimeInactive = new Date(connectedTimeNow.getTime() - ACTIVE_CONNECTION_TIME);
describe('~/environments/components/kubernetes_agent_info.vue', () => {
let wrapper;
let agentQueryResponse;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAgentLink = () => wrapper.findComponent(GlLink);
const findAgentStatus = () => wrapper.findByTestId('agent-status');
const findAgentStatusIcon = () => findAgentStatus().findComponent(GlIcon);
const findAgentLastUsedDate = () => wrapper.findByTestId('agent-last-used-date');
const findAlert = () => wrapper.findComponent(GlAlert);
const createWrapper = ({ tokens = [], queryResponse = null } = {}) => {
const clusterAgent = { ...mockClusterAgent, tokens: { nodes: tokens } };
agentQueryResponse =
queryResponse ||
jest.fn().mockResolvedValue({ data: { project: { id: 'project-1', clusterAgent } } });
const apolloProvider = createMockApollo([[getK8sClusterAgentQuery, agentQueryResponse]]);
wrapper = extendedWrapper(
shallowMount(KubernetesAgentInfo, {
apolloProvider,
propsData,
stubs: { TimeAgoTooltip, GlSprintf },
}),
);
};
describe('default', () => {
beforeEach(() => {
createWrapper();
});
it('shows loading icon while fetching the agent details', async () => {
expect(findLoadingIcon().exists()).toBe(true);
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
});
it('sends expected params', async () => {
await waitForPromises();
const variables = {
agentName: propsData.agentName,
projectPath: propsData.agentProjectPath,
tokenStatus: TOKEN_STATUS_ACTIVE,
};
expect(agentQueryResponse).toHaveBeenCalledWith(variables);
});
it('renders the agent name with the link', async () => {
await waitForPromises();
expect(findAgentLink().attributes('href')).toBe(mockClusterAgent.webPath);
expect(findAgentLink().text()).toContain(mockClusterAgent.id);
});
});
describe.each`
lastUsedAt | status | lastUsedText
${null} | ${'unused'} | ${KubernetesAgentInfo.i18n.neverConnectedText}
${connectedTimeNow} | ${'active'} | ${'just now'}
${connectedTimeInactive} | ${'inactive'} | ${'8 minutes ago'}
`('when agent connection status is "$status"', ({ lastUsedAt, status, lastUsedText }) => {
beforeEach(async () => {
const tokens = [{ id: 'token-id', lastUsedAt }];
createWrapper({ tokens });
await waitForPromises();
});
it('displays correct status text', () => {
expect(findAgentStatus().text()).toBe(AGENT_STATUSES[status].name);
});
it('displays correct status icon', () => {
expect(findAgentStatusIcon().props('name')).toBe(AGENT_STATUSES[status].icon);
expect(findAgentStatusIcon().attributes('class')).toBe(AGENT_STATUSES[status].class);
});
it('displays correct last used date status', () => {
expect(findAgentLastUsedDate().text()).toBe(lastUsedText);
});
});
describe('when the agent query has errored', () => {
beforeEach(() => {
createWrapper({ clusterAgent: null, queryResponse: jest.fn().mockRejectedValue() });
return waitForPromises();
});
it('displays an alert message', () => {
expect(findAlert().text()).toBe(KubernetesAgentInfo.i18n.loadingError);
});
});
});

View File

@ -0,0 +1,84 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlCollapse, GlButton } from '@gitlab/ui';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info.vue';
const agent = {
project: 'agent-project',
id: '1',
name: 'agent-name',
};
const propsData = {
agentId: agent.id,
agentName: agent.name,
agentProjectPath: agent.project,
};
describe('~/environments/components/kubernetes_overview.vue', () => {
let wrapper;
const findCollapse = () => wrapper.findComponent(GlCollapse);
const findCollapseButton = () => wrapper.findComponent(GlButton);
const findAgentInfo = () => wrapper.findComponent(KubernetesAgentInfo);
const createWrapper = () => {
wrapper = shallowMount(KubernetesOverview, {
propsData,
});
};
const toggleCollapse = async () => {
findCollapseButton().vm.$emit('click');
await nextTick();
};
describe('default', () => {
beforeEach(() => {
createWrapper();
});
it('renders the kubernetes overview title', () => {
expect(wrapper.text()).toBe(KubernetesOverview.i18n.sectionTitle);
});
});
describe('collapse', () => {
beforeEach(() => {
createWrapper();
});
it('is collapsed by default', () => {
expect(findCollapse().props('visible')).toBeUndefined();
expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.expand);
expect(findCollapseButton().props('icon')).toBe('chevron-right');
});
it("doesn't render components when the collapse is not visible", () => {
expect(findAgentInfo().exists()).toBe(false);
});
it('opens on click', async () => {
findCollapseButton().vm.$emit('click');
await nextTick();
expect(findCollapse().attributes('visible')).toBe('true');
expect(findCollapseButton().attributes('aria-label')).toBe(KubernetesOverview.i18n.collapse);
expect(findCollapseButton().props('icon')).toBe('chevron-down');
});
});
describe('when section is expanded', () => {
it('renders kubernetes agent info', async () => {
createWrapper();
await toggleCollapse();
expect(findAgentInfo().props()).toEqual({
agentName: agent.name,
agentId: agent.id,
agentProjectPath: agent.project,
});
});
});
});

View File

@ -9,7 +9,8 @@ import { __, s__, sprintf } from '~/locale';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import { resolvedEnvironment, rolloutStatus } from './graphql/mock_data';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
Vue.use(VueApollo);
@ -20,15 +21,16 @@ describe('~/environments/components/new_environment_item.vue', () => {
return createMockApollo();
};
const createWrapper = ({ propsData = {}, apolloProvider } = {}) =>
const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) =>
mountExtended(EnvironmentItem, {
apolloProvider,
propsData: { environment: resolvedEnvironment, ...propsData },
provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1' },
provide: { helpPagePath: '/help', projectId: '1', projectPath: '/1', ...provideData },
stubs: { transition: stubTransition() },
});
const findDeployment = () => wrapper.findComponent(Deployment);
const findKubernetesOverview = () => wrapper.findComponent(KubernetesOverview);
const expandCollapsedSection = async () => {
const button = wrapper.findByRole('button', { name: __('Expand') });
@ -37,10 +39,6 @@ describe('~/environments/components/new_environment_item.vue', () => {
return button;
};
afterEach(() => {
wrapper?.destroy();
});
it('displays the name when not in a folder', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
@ -157,7 +155,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
describe('stop', () => {
it('shows a buton to stop the environment if the environment is available', () => {
it('shows a button to stop the environment if the environment is available', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const stop = wrapper.findByRole('button', { name: s__('Environments|Stop environment') });
@ -165,7 +163,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(stop.exists()).toBe(true);
});
it('does not show a buton to stop the environment if the environment is stopped', () => {
it('does not show a button to stop the environment if the environment is stopped', () => {
wrapper = createWrapper({
propsData: { environment: { ...resolvedEnvironment, canStop: false } },
apolloProvider: createApolloProvider(),
@ -515,4 +513,71 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(deployBoard.exists()).toBe(false);
});
});
describe('kubernetes overview', () => {
const environmentWithAgent = {
...resolvedEnvironment,
agent,
};
it('should render if the feature flag is enabled and the environment has an agent object with the required data specified', () => {
wrapper = createWrapper({
propsData: { environment: environmentWithAgent },
provideData: {
glFeatures: {
kasUserAccessProject: true,
},
},
apolloProvider: createApolloProvider(),
});
expandCollapsedSection();
expect(findKubernetesOverview().props()).toMatchObject({
agentProjectPath: agent.project,
agentName: agent.name,
agentId: agent.id,
});
});
it('should not render if the feature flag is not enabled', () => {
wrapper = createWrapper({
propsData: { environment: environmentWithAgent },
apolloProvider: createApolloProvider(),
});
expandCollapsedSection();
expect(findKubernetesOverview().exists()).toBe(false);
});
it('should not render if the environment has no agent object', () => {
wrapper = createWrapper({
apolloProvider: createApolloProvider(),
});
expandCollapsedSection();
expect(findKubernetesOverview().exists()).toBe(false);
});
it('should not render if the environment has an agent object without agent id specified', () => {
const environment = {
...resolvedEnvironment,
agent: {
project: agent.project,
name: agent.name,
},
};
wrapper = createWrapper({
propsData: { environment },
apolloProvider: createApolloProvider(),
});
expandCollapsedSection();
expect(findKubernetesOverview().exists()).toBe(false);
});
});
});

View File

@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_CREATED, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
jest.mock('~/flash');
jest.mock('~/alert');
describe('feature highlight helper', () => {
describe('dismiss', () => {
@ -26,7 +26,7 @@ describe('feature highlight helper', () => {
await expect(dismiss(endpoint, highlightId)).resolves.toEqual(expect.anything());
});
it('triggers flash when dismiss request fails', async () => {
it('triggers an alert when dismiss request fails', async () => {
mockAxios
.onPost(endpoint, { feature_name: highlightId })
.replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);

View File

@ -22,7 +22,7 @@ import {
setSortPreferenceMutationResponseWithErrors,
urlParams,
} from 'jest/issues/list/mock_data';
import { createAlert, VARIANT_INFO } from '~/flash';
import { createAlert, VARIANT_INFO } from '~/alert';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
@ -72,7 +72,7 @@ import('~/issuable');
import('~/users_select');
jest.mock('@sentry/browser');
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
describe('CE IssuesListApp component', () => {

View File

@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/issues/related_merge_requests/store/actions';
import * as types from '~/issues/related_merge_requests/store/mutation_types';
jest.mock('~/flash');
jest.mock('~/alert');
describe('RelatedMergeRequest store actions', () => {
let state;

View File

@ -5,7 +5,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import {
IssuableStatusText,
STATUS_CLOSED,
@ -34,7 +34,7 @@ import {
zoomMeetingUrl,
} from '../mock_data/mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/issues/show/event_hub');
jest.mock('~/lib/utils/url_utility');
jest.mock('~/behaviors/markdown/render_gfm');

View File

@ -7,7 +7,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import Description from '~/issues/show/components/description.vue';
import eventHub from '~/issues/show/event_hub';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@ -23,7 +23,7 @@ import {
} from 'jest/work_items/mock_data';
import { descriptionProps as initialProps, descriptionHtmlWithList } from '../mock_data/mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
updateHistory: jest.fn(),

View File

@ -9,7 +9,7 @@ import createTimelineEventMutation from '~/issues/show/components/incidents/grap
import getTimelineEvents from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
import { timelineFormI18n } from '~/issues/show/components/incidents/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { useFakeDate } from 'helpers/fake_date';
import {
timelineEventsCreateEventResponse,
@ -19,7 +19,7 @@ import {
Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/alert');
const fakeDate = '2020-07-08T00:00:00.000Z';

View File

@ -10,12 +10,12 @@ import {
TIMELINE_EVENT_TAGS,
timelineEventTagsI18n,
} from '~/issues/show/components/incidents/constants';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { useFakeDate } from 'helpers/fake_date';
Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/alert');
const fakeDate = '2020-07-08T00:00:00.000Z';

View File

@ -11,7 +11,7 @@ import deleteTimelineEventMutation from '~/issues/show/components/incidents/grap
import editTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/edit_timeline_event.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { useFakeDate } from 'helpers/fake_date';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import {
mockEvents,
timelineEventsDeleteEventResponse,
@ -26,7 +26,7 @@ import {
Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
const mockConfirmAction = ({ confirmed }) => {

View File

@ -8,13 +8,13 @@ import IncidentTimelineEventsList from '~/issues/show/components/incidents/timel
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
import timelineEventsQuery from '~/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { timelineTabI18n } from '~/issues/show/components/incidents/constants';
import { timelineEventsQueryListResponse, timelineEventsQueryEmptyResponse } from './mock_data';
Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/alert');
const graphQLError = new Error('GraphQL error');
const listResponse = jest.fn().mockResolvedValue(timelineEventsQueryListResponse);

View File

@ -5,10 +5,10 @@ import {
getUtcShiftedDate,
getPreviousEventTags,
} from '~/issues/show/components/incidents/utils';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { mockTimelineEventTags } from './mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
describe('incident utils', () => {
describe('display and log error', () => {

View File

@ -4,13 +4,13 @@ import Cookies from '~/lib/utils/cookies';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { INTERACTIVE_RESOLVE_MODE, EDIT_RESOLVE_MODE } from '~/merge_conflicts/constants';
import * as actions from '~/merge_conflicts/store/actions';
import * as types from '~/merge_conflicts/store/mutation_types';
import { restoreFileLinesState, markLine, decorateFiles } from '~/merge_conflicts/utils';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/merge_conflicts/utils');
jest.mock('~/lib/utils/cookies');
@ -114,7 +114,7 @@ describe('merge conflicts actions', () => {
expect(window.location.assign).toHaveBeenCalledWith('hrefPath');
});
it('on errors shows flash', async () => {
it('on errors shows an alert', async () => {
mock.onPost(resolveConflictsPath).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.submitResolvedConflicts,

View File

@ -6,7 +6,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { kebabCase } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import * as urlUtility from '~/lib/utils/url_utility';
import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -14,7 +14,7 @@ import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_name
import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('ForkForm component', () => {
@ -552,7 +552,7 @@ describe('ForkForm component', () => {
expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl);
});
it('display flash when POST is unsuccessful', async () => {
it('displays an alert when POST is unsuccessful', async () => {
const dummyError = 'Fork project failed';
jest.spyOn(axios, 'post').mockRejectedValue(dummyError);

View File

@ -3,11 +3,11 @@ import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql';
import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
jest.mock('~/flash');
jest.mock('~/alert');
describe('ProjectNamespace component', () => {
let wrapper;
@ -152,7 +152,7 @@ describe('ProjectNamespace component', () => {
await nextTick();
});
it('creates a flash message and captures the error', () => {
it('creates an alert message and captures the error', () => {
expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while loading data. Please refresh the page to try again.',
captureError: true,

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe BulkImports::NdjsonPipeline do
RSpec.describe BulkImports::NdjsonPipeline, feature_category: :importers do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
@ -150,13 +150,63 @@ RSpec.describe BulkImports::NdjsonPipeline do
describe '#load' do
context 'when object is not persisted' do
it 'saves the object using RelationObjectSaver' do
object = double(persisted?: false, new_record?: true)
allow(subject).to receive(:relation_definition)
expect_next_instance_of(Gitlab::ImportExport::Base::RelationObjectSaver) do |saver|
expect(saver).to receive(:execute)
end
subject.load(nil, object)
end
context 'when object is invalid' do
it 'captures invalid subrelations' do
entity = create(:bulk_import_entity, group: group)
tracker = create(:bulk_import_tracker, entity: entity)
context = BulkImports::Pipeline::Context.new(tracker)
allow(subject).to receive(:context).and_return(context)
object = group.labels.new(priorities: [LabelPriority.new])
object.validate
allow_next_instance_of(Gitlab::ImportExport::Base::RelationObjectSaver) do |saver|
allow(saver).to receive(:execute)
allow(saver).to receive(:invalid_subrelations).and_return(object.priorities)
end
subject.load(context, object)
failure = entity.failures.first
expect(failure.pipeline_class).to eq(tracker.pipeline_name)
expect(failure.exception_class).to eq('RecordInvalid')
expect(failure.exception_message).to eq("Project can't be blank, Priority can't be blank, and Priority is not a number")
end
end
end
context 'when object is persisted' do
it 'saves the object' do
object = double(persisted?: false)
object = double(new_record?: false, invalid?: false)
expect(object).to receive(:save!)
subject.load(nil, object)
end
context 'when object is invalid' do
it 'raises ActiveRecord::RecordInvalid exception' do
object = build_stubbed(:issue)
expect(Gitlab::Import::Errors).to receive(:merge_nested_errors).with(object)
expect { subject.load(nil, object) }.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
context 'when object is missing' do

View File

@ -6,7 +6,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
include RepoHelpers
include StubRequests
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project) { create(:project, :small_repo) }
let_it_be(:user) { project.owner }
let(:context) do
@ -38,7 +38,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
}
end
around(:all) do |example|
around do |example|
create_and_delete_files(project, project_files) do
example.run
end
@ -84,8 +84,8 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
end
context 'when files are project files' do
let_it_be(:included_project1) { create(:project, :repository, namespace: project.namespace, creator: user) }
let_it_be(:included_project2) { create(:project, :repository, namespace: project.namespace, creator: user) }
let_it_be(:included_project1) { create(:project, :small_repo, namespace: project.namespace, creator: user) }
let_it_be(:included_project2) { create(:project, :small_repo, namespace: project.namespace, creator: user) }
let(:files) do
[
@ -107,7 +107,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
]
end
around(:all) do |example|
around do |example|
create_and_delete_files(included_project1, project_files) do
create_and_delete_files(included_project2, project_files) do
example.run
@ -115,10 +115,12 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
end
end
it 'returns an array of file objects' do
it 'returns an array of valid file objects' do
expect(process.map(&:location)).to contain_exactly(
'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml', 'myfolder/file1.yml', 'myfolder/file2.yml'
)
expect(process.all?(&:valid?)).to be_truthy
end
it 'adds files to the expandset' do
@ -139,7 +141,9 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category:
projects_queries = queries.occurrences_starting_with('SELECT "projects"')
access_check_queries = queries.occurrences_starting_with('SELECT MAX("project_authorizations"."access_level")')
expect(projects_queries.values.sum).to eq(1)
# We could not reduce the number of projects queries because we need to call project for
# the `can_access_local_content?` and `sha` BatchLoaders.
expect(projects_queries.values.sum).to eq(2)
expect(access_check_queries.values.sum).to eq(2)
end

View File

@ -1794,47 +1794,37 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
end
describe '#license' do
where(from_gitaly: [true, false])
with_them do
subject(:license) { repository.license(from_gitaly) }
subject(:license) { repository.license }
context 'when no license file can be found' do
let_it_be(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
context 'when no license file can be found' do
let_it_be(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
before do
project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
end
it { is_expected.to be_nil }
before do
project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
end
context 'when an mit license is found' do
it { is_expected.to have_attributes(key: 'mit') }
end
context 'when license is not recognized ' do
let_it_be(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
before do
project.repository.update_file(
project.owner,
'LICENSE',
'This software is licensed under the Dummy license.',
message: 'Update license',
branch_name: 'master')
end
it { is_expected.to have_attributes(key: 'other', nickname: 'LICENSE') }
end
it { is_expected.to be_nil }
end
it 'does not crash when license is invalid' do
expect(Licensee::License).to receive(:new)
.and_raise(Licensee::InvalidLicense)
context 'when an mit license is found' do
it { is_expected.to have_attributes(key: 'mit') }
end
expect(repository.license(false)).to be_nil
context 'when license is not recognized ' do
let_it_be(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw_repository }
before do
project.repository.update_file(
project.owner,
'LICENSE',
'This software is licensed under the Dummy license.',
message: 'Update license',
branch_name: 'master')
end
it { is_expected.to have_attributes(key: 'other', nickname: 'LICENSE') }
end
end

View File

@ -8,6 +8,10 @@ RSpec.describe Gitlab::GithubImport::Clients::Proxy, :manage, feature_category:
let(:access_token) { 'test_token' }
let(:client_options) { { foo: :bar } }
it { expect(client).to delegate_method(:each_object).to(:client) }
it { expect(client).to delegate_method(:user).to(:client) }
it { expect(client).to delegate_method(:octokit).to(:client) }
describe '#repos' do
let(:search_text) { 'search text' }
let(:pagination_options) { { limit: 10 } }

View File

@ -0,0 +1,49 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::ProjectRelationType, :manage, feature_category: :importers do
subject(:project_relation_type) { described_class.new(client) }
let(:octokit) { instance_double(Octokit::Client) }
let(:client) do
instance_double(Gitlab::GithubImport::Clients::Proxy, octokit: octokit, user: { login: 'nickname' })
end
describe '#for', :use_clean_rails_redis_caching do
before do
allow(client).to receive(:each_object).with(:organizations).and_yield({ login: 'great-org' })
allow(octokit).to receive(:access_token).and_return('stub')
end
context "when it's user owned repo" do
let(:import_source) { 'nickname/repo_name' }
it { expect(project_relation_type.for(import_source)).to eq 'owned' }
end
context "when it's organization repo" do
let(:import_source) { 'great-org/repo_name' }
it { expect(project_relation_type.for(import_source)).to eq 'organization' }
end
context "when it's user collaborated repo" do
let(:import_source) { 'some-another-namespace/repo_name' }
it { expect(project_relation_type.for(import_source)).to eq 'collaborated' }
end
context 'with cache' do
let(:import_source) { 'some-another-namespace/repo_name' }
it 'calls client only once during 5 minutes timeframe', :request_store do
expect(project_relation_type.for(import_source)).to eq 'collaborated'
expect(project_relation_type.for('another/repo')).to eq 'collaborated'
expect(client).to have_received(:each_object).once
expect(client).to have_received(:user).once
end
end
end
end

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Import::Errors, feature_category: :importers do
let_it_be(:project) { create(:project) }
describe '.merge_nested_errors' do
it 'merges nested collection errors' do
issue = project.issues.new(
title: 'test',
notes: [
Note.new(
award_emoji: [AwardEmoji.new(name: 'test')]
)
],
sentry_issue: SentryIssue.new
)
issue.validate
expect(issue.errors.full_messages)
.to contain_exactly(
"Author can't be blank",
"Notes is invalid",
"Sentry issue sentry issue identifier can't be blank"
)
described_class.merge_nested_errors(issue)
expect(issue.errors.full_messages)
.to contain_exactly(
"Notes is invalid",
"Author can't be blank",
"Sentry issue sentry issue identifier can't be blank",
"Award emoji is invalid",
"Note can't be blank",
"Project can't be blank",
"Noteable can't be blank",
"Author can't be blank",
"Project does not match noteable project",
"User can't be blank",
"Awardable can't be blank",
"Name is not a valid emoji name"
)
end
end
end

View File

@ -82,24 +82,13 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver, feature_category
it 'saves valid subrelations and logs invalid subrelation' do
expect(relation_object.notes).to receive(:<<).twice.and_call_original
expect(relation_object).to receive(:save).and_call_original
expect(Gitlab::Import::Logger)
.to receive(:info)
.with(
message: '[Project/Group Import] Invalid subrelation',
project_id: project.id,
relation_key: 'issues',
error_messages: "Project does not match noteable project"
)
saver.execute
issue = project.issues.last
import_failure = project.import_failures.last
expect(invalid_note.persisted?).to eq(false)
expect(issue.notes.count).to eq(5)
expect(import_failure.source).to eq('RelationObjectSaver#save!')
expect(import_failure.exception_message).to eq('Project does not match noteable project')
end
context 'when invalid subrelation can still be persisted' do
@ -111,7 +100,6 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver, feature_category
it 'saves the subrelation' do
expect(approval_1.valid?).to eq(false)
expect(Gitlab::Import::Logger).not_to receive(:info)
saver.execute
@ -128,24 +116,10 @@ RSpec.describe Gitlab::ImportExport::Base::RelationObjectSaver, feature_category
let(:invalid_priority) { build(:label_priority, priority: -1) }
let(:relation_object) { build(:group_label, group: importable, title: 'test', priorities: valid_priorities + [invalid_priority]) }
it 'logs invalid subrelation for a group' do
expect(Gitlab::Import::Logger)
.to receive(:info)
.with(
message: '[Project/Group Import] Invalid subrelation',
group_id: importable.id,
relation_key: 'labels',
error_messages: 'Priority must be greater than or equal to 0'
)
it 'saves relation without invalid subrelations' do
saver.execute
label = importable.labels.last
import_failure = importable.import_failures.last
expect(label.priorities.count).to eq(5)
expect(import_failure.source).to eq('RelationObjectSaver#save!')
expect(import_failure.exception_message).to eq('Priority must be greater than or equal to 0')
expect(importable.labels.last.priorities.count).to eq(5)
end
end
end

View File

@ -9,7 +9,7 @@
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do
RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer, feature_category: :importers do
let(:group) { create(:group).tap { |g| g.add_owner(user) } }
let(:importable) { create(:group, parent: group) }
@ -60,4 +60,81 @@ RSpec.describe Gitlab::ImportExport::Group::RelationTreeRestorer do
subject
end
describe 'relation object saving' do
let(:importable) { create(:group) }
let(:relation_reader) do
Gitlab::ImportExport::Json::LegacyReader::File.new(
path,
relation_names: [:labels])
end
before do
allow(shared.logger).to receive(:info).and_call_original
allow(relation_reader).to receive(:consume_relation).and_call_original
allow(relation_reader)
.to receive(:consume_relation)
.with(nil, 'labels')
.and_return([[label, 0]])
end
context 'when relation object is new' do
context 'when relation object has invalid subrelations' do
let(:label) do
{
'title' => 'test',
'priorities' => [LabelPriority.new, LabelPriority.new],
'type' => 'GroupLabel'
}
end
it 'logs invalid subrelations' do
expect(shared.logger)
.to receive(:info)
.with(
message: '[Project/Group Import] Invalid subrelation',
group_id: importable.id,
relation_key: 'labels',
error_messages: "Project can't be blank, Priority can't be blank, and Priority is not a number"
)
subject
label = importable.labels.first
failure = importable.import_failures.first
expect(importable.import_failures.count).to eq(2)
expect(label.title).to eq('test')
expect(failure.exception_class).to eq('ActiveRecord::RecordInvalid')
expect(failure.source).to eq('RelationTreeRestorer#save_relation_object')
expect(failure.exception_message)
.to eq("Project can't be blank, Priority can't be blank, and Priority is not a number")
end
end
end
context 'when relation object is persisted' do
context 'when relation object is invalid' do
let(:label) { create(:group_label, group: group, title: 'test') }
it 'saves import failure with nested errors' do
label.priorities << [LabelPriority.new, LabelPriority.new]
subject
failure = importable.import_failures.first
expect(importable.labels.count).to eq(0)
expect(importable.import_failures.count).to eq(1)
expect(failure.exception_class).to eq('ActiveRecord::RecordInvalid')
expect(failure.source).to eq('process_relation_item!')
expect(failure.exception_message)
.to eq("Validation failed: Priorities is invalid, Project can't be blank, Priority can't be blank, " \
"Priority is not a number, Project can't be blank, Priority can't be blank, " \
"Priority is not a number")
end
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::ImportExport::ImportFailureService do
RSpec.describe Gitlab::ImportExport::ImportFailureService, feature_category: :importers do
let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
let(:label) { create(:label) }
let(:subject) { described_class.new(importable) }

View File

@ -2,7 +2,7 @@
require 'rake_helper'
RSpec.describe Gitlab::ImportExport::Project::ImportTask, :request_store, :silence_stdout do
RSpec.describe Gitlab::ImportExport::Project::ImportTask, :request_store, :silence_stdout, feature_category: :importers do
let(:username) { 'root' }
let(:namespace_path) { username }
let!(:user) { create(:user, username: username) }

View File

@ -3,12 +3,13 @@
require 'spec_helper'
RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:namespace) { create(:group) }
let_it_be(:project_1) { create(:project, namespace: namespace) }
let_it_be(:project_2) { create(:project, namespace: namespace) }
let_it_be(:project_3) { create(:project) }
let_it_be(:user) { create(:user) }
let(:list) { described_class.new(namespace) }
let(:list) { described_class.new(namespace, user) }
describe '#new' do
context 'when namespace is not a root namespace' do
@ -23,16 +24,42 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do
describe '#resources' do
subject(:resources) { list.resources }
context 'when the namespace has no catalog resources' do
it { is_expected.to be_empty }
context 'when the user has access to all projects in the namespace' do
before do
namespace.add_developer(user)
end
context 'when the namespace has no catalog resources' do
it { is_expected.to be_empty }
end
context 'when the namespace has catalog resources' do
let!(:resource) { create(:catalog_resource, project: project_1) }
let!(:other_namespace_resource) { create(:catalog_resource, project: project_3) }
it 'contains only catalog resources for projects in that namespace' do
is_expected.to contain_exactly(resource)
end
end
end
context 'when the namespace has catalog resources' do
context 'when the user only has access to some projects in the namespace' do
let!(:resource_1) { create(:catalog_resource, project: project_1) }
let!(:resource_2) { create(:catalog_resource, project: project_2) }
before do
project_1.add_developer(user)
end
it 'only returns catalog resources for projects the user has access to' do
is_expected.to contain_exactly(resource_1)
end
end
context 'when the user does not have access to the namespace' do
let!(:resource) { create(:catalog_resource, project: project_1) }
it 'contains only catalog resources for projects in that namespace' do
is_expected.to contain_exactly(resource)
end
it { is_expected.to be_empty }
end
end
end

View File

@ -7,6 +7,12 @@ RSpec.describe Projects::DataTransfer, feature_category: :source_code_management
it { expect(subject).to be_valid }
# tests DataTransferCounterAttribute with the appropiate attributes
it_behaves_like CounterAttribute,
%i[repository_egress artifacts_egress packages_egress registry_egress] do
let(:model) { create(:project_data_transfer, project: project) }
end
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:namespace) }

View File

@ -1068,7 +1068,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
describe "#delete_file" do
let(:project) { create(:project, :repository) }
let_it_be(:project) { create(:project, :repository) }
it 'removes file successfully' do
expect do
@ -1469,46 +1469,38 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
[true, false].each do |ff|
context "with feature flag license_from_gitaly=#{ff}" do
before do
stub_feature_flags(license_from_gitaly: ff)
end
describe '#license', :use_clean_rails_memory_store_caching, :clean_gitlab_redis_cache do
let(:project) { create(:project, :repository) }
describe '#license', :use_clean_rails_memory_store_caching, :clean_gitlab_redis_cache do
let(:project) { create(:project, :repository) }
before do
repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
end
before do
repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
end
it 'returns nil when no license is detected' do
expect(repository.license).to be_nil
end
it 'returns nil when no license is detected' do
expect(repository.license).to be_nil
end
it 'returns nil when the repository does not exist' do
expect(repository).to receive(:exists?).and_return(false)
it 'returns nil when the repository does not exist' do
expect(repository).to receive(:exists?).and_return(false)
expect(repository.license).to be_nil
end
expect(repository.license).to be_nil
end
it 'returns other when the content is not recognizable' do
repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
it 'returns other when the content is not recognizable' do
repository.create_file(user, 'LICENSE', 'Gitlab B.V.',
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to eq('other')
end
expect(repository.license_key).to eq('other')
end
it 'returns the license' do
license = Licensee::License.new('mit')
repository.create_file(user, 'LICENSE',
license.content,
message: 'Add LICENSE', branch_name: 'master')
it 'returns the license' do
license = Licensee::License.new('mit')
repository.create_file(user, 'LICENSE',
license.content,
message: 'Add LICENSE', branch_name: 'master')
expect(repository.license_key).to eq(license.key)
end
end
expect(repository.license_key).to eq(license.key)
end
end
@ -2349,7 +2341,6 @@ RSpec.describe Repository, feature_category: :source_code_management do
:contribution_guide,
:changelog,
:license_blob,
:license_licensee,
:license_gitaly,
:gitignore,
:gitlab_ci_yml,
@ -3004,11 +2995,10 @@ RSpec.describe Repository, feature_category: :source_code_management do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches)
.with(%i(readme_path license_blob license_licensee license_gitaly))
.with(%i(readme_path license_blob license_gitaly))
expect(repository).to receive(:readme_path)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_licensee)
expect(repository).to receive(:license_gitaly)
repository.refresh_method_caches(%i(readme license))

View File

@ -5,10 +5,11 @@ require 'spec_helper'
RSpec.describe ProjectImportEntity, feature_category: :importers do
include ImportHelper
let_it_be(:project) { create(:project, import_status: :started, import_source: 'namespace/project') }
let_it_be(:project) { create(:project, import_status: :started, import_source: 'import_user/project') }
let(:provider_url) { 'https://provider.com' }
let(:entity) { described_class.represent(project, provider_url: provider_url) }
let(:client) { nil }
let(:entity) { described_class.represent(project, provider_url: provider_url, client: client) }
before do
create(:import_failure, project: project)
@ -23,6 +24,31 @@ RSpec.describe ProjectImportEntity, feature_category: :importers do
expect(subject[:human_import_status_name]).to eq(project.human_import_status_name)
expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, project[:import_source]))
expect(subject[:import_error]).to eq(nil)
expect(subject[:relation_type]).to eq(nil)
end
context 'when client option present', :clean_gitlab_redis_cache do
let(:octokit) { instance_double(Octokit::Client, access_token: 'stub') }
let(:client) do
instance_double(
::Gitlab::GithubImport::Clients::Proxy,
user: { login: 'import_user' }, octokit: octokit
)
end
it 'includes relation_type' do
expect(subject[:relation_type]).to eq('owned')
end
context 'with remove_legacy_github_client FF is disabled' do
before do
stub_feature_flags(remove_legacy_github_client: false)
end
it "doesn't include relation_type" do
expect(subject[:relation_type]).to eq(nil)
end
end
end
context 'when import is failed' do

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