Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2cd5f04547
commit
b50c9d31e3
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
8eb2d65be9607663316e0939d2550fa22df98ea0
|
||||
6d7521c579ecb1b0a76c4c29e12ce9b72dd4057e
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.gd {
|
||||
.gi {
|
||||
background-color: var(--diff-addition-color);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(':')
|
||||
|
|
|
|||
|
|
@ -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/*'
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ module Gitlab
|
|||
}
|
||||
)
|
||||
end
|
||||
alias_method :octokit, :api
|
||||
|
||||
def client
|
||||
unless config
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ describe('~/environments/components/environments_folder.vue', () => {
|
|||
...propsData,
|
||||
},
|
||||
stubs: { transition: stubTransition() },
|
||||
provide: { helpPagePath: '/help' },
|
||||
provide: { helpPagePath: '/help', projectId: '1' },
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
|
|
|||
|
|
@ -798,3 +798,9 @@ export const resolvedDeploymentDetails = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const agent = {
|
||||
project: 'agent-project',
|
||||
id: '1',
|
||||
name: 'agent-name',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue