Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-01-03 21:13:21 +00:00
parent b87af16bf2
commit b63258f304
31 changed files with 622 additions and 292 deletions

View File

@ -15,7 +15,7 @@ include:
gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"
allure_job_name: "${QA_RUN_TYPE}"
- project: gitlab-org/quality/pipeline-common
ref: 8.3.0
ref: 8.3.2
file:
- /ci/base.gitlab-ci.yml
- /ci/knapsack-report.yml

View File

@ -49,7 +49,7 @@ export default {
return getIdFromGraphQLId(this.resource.id);
},
hasLatestVersion() {
return this.latestVersion?.tagName;
return this.latestVersion?.name;
},
hasPipelineStatus() {
return this.pipelineStatus?.text;
@ -58,7 +58,7 @@ export default {
return this.resource.latestVersion;
},
versionBadgeText() {
return this.latestVersion.tagName;
return this.latestVersion.name;
},
webPath() {
return cleanLeadingSeparator(this.resource?.webPath);
@ -92,7 +92,7 @@ export default {
v-if="hasLatestVersion"
size="sm"
class="gl-ml-3 gl-my-1"
:href="latestVersion.tagPath"
:href="latestVersion.path"
>
{{ versionBadgeText }}
</gl-badge>

View File

@ -67,8 +67,8 @@ export default {
releasedAt() {
return getTimeago().format(this.latestVersion?.releasedAt);
},
tagName() {
return this.latestVersion?.tagName || this.$options.i18n.unreleased;
name() {
return this.latestVersion?.name || this.$options.i18n.unreleased;
},
webPath() {
return cleanLeadingSeparator(this.resource?.webPath);
@ -117,7 +117,7 @@ export default {
<b> {{ resource.name }}</b>
</gl-link>
<div class="gl-display-flex gl-flex-grow-1 gl-md-justify-content-space-between">
<gl-badge size="sm" class="gl-h-5 gl-align-self-center">{{ tagName }}</gl-badge>
<gl-badge size="sm" class="gl-h-5 gl-align-self-center">{{ name }}</gl-badge>
<span class="gl-display-flex gl-align-items-center gl-ml-5">
<span
v-gl-tooltip.top

View File

@ -7,8 +7,8 @@ fragment CatalogResourceFields on CiCatalogResource {
starCount
latestVersion {
id
tagName
tagPath
name
path
releasedAt
author {
id

View File

@ -22,7 +22,7 @@ query getCiCatalogResourceDetails($fullPath: ID!) {
}
}
}
tagName
name
releasedAt
}
}

View File

@ -1,9 +1,20 @@
<script>
import { GlButton, GlModal, GlModalDirective, GlCard, GlIcon } from '@gitlab/ui';
import {
GlButton,
GlModal,
GlModalDirective,
GlCard,
GlIcon,
GlDisclosureDropdown,
GlCollapsibleListbox,
GlFormGroup,
} from '@gitlab/ui';
import { createAlert } from '~/alert';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { expandSection } from '~/settings_panels';
import { scrollToElement } from '~/lib/utils/common_utils';
import createBranchRuleMutation from './graphql/mutations/create_branch_rule.mutation.graphql';
import BranchRule from './components/branch_rule.vue';
import { I18N, PROTECTED_BRANCHES_ANCHOR, BRANCH_PROTECTION_MODAL_ID } from './constants';
@ -14,12 +25,16 @@ export default {
BranchRule,
GlButton,
GlModal,
GlFormGroup,
GlCard,
GlIcon,
GlCollapsibleListbox,
GlDisclosureDropdown,
},
directives: {
GlModal: GlModalDirective,
},
mixins: [glFeatureFlagsMixin()],
apollo: {
branchRules: {
query: branchRulesQuery,
@ -38,18 +53,87 @@ export default {
},
inject: {
projectPath: { default: '' },
branchRulesPath: { default: '' },
},
data() {
return {
branchRules: [],
branchRuleName: '',
searchQuery: '',
};
},
computed: {
addRuleItems() {
return [{ text: this.$options.i18n.branchName, action: () => this.openCreateRuleModal() }];
},
createRuleItems() {
return this.isWildcardAvailable ? [this.wildcardItem] : this.filteredOpenBranches;
},
filteredOpenBranches() {
const openBranches = window.gon.open_branches.map((item) => ({
text: item.text,
value: item.text,
}));
return openBranches.filter((item) => item.text.includes(this.searchQuery));
},
wildcardItem() {
return { text: this.$options.i18n.createWildcard, value: this.searchQuery };
},
isWildcardAvailable() {
return this.searchQuery.includes('*');
},
createRuleText() {
return this.branchRuleName || this.$options.i18n.branchNamePlaceholder;
},
branchRuleEditPath() {
return `${this.branchRulesPath}?branch=${encodeURIComponent(this.branchRuleName)}`;
},
primaryProps() {
return {
text: this.$options.i18n.createProtectedBranch,
attributes: {
variant: 'confirm',
disabled: !this.branchRuleName,
},
};
},
cancelProps() {
return {
text: this.$options.i18n.createBranchRule,
};
},
},
methods: {
showProtectedBranches() {
// Protected branches section is on the same page as the branch rules section.
expandSection(this.$options.protectedBranchesAnchor);
scrollToElement(this.$options.protectedBranchesAnchor);
},
openCreateRuleModal() {
this.$refs[this.$options.modalId].show();
},
handleBranchRuleSearch(query) {
this.searchQuery = query;
},
addBranchRule() {
this.$apollo
.mutate({
mutation: createBranchRuleMutation,
variables: {
projectPath: this.projectPath,
name: this.branchRuleName,
},
})
.then(() => {
window.location.assign(this.branchRuleEditPath);
})
.catch(() => {
createAlert({ message: this.$options.i18n.createBranchRuleError });
});
},
selectBranchRuleName(branchName) {
this.branchRuleName = branchName;
},
},
modalId: BRANCH_PROTECTION_MODAL_ID,
protectedBranchesAnchor: PROTECTED_BRANCHES_ANCHOR,
@ -72,7 +156,15 @@ export default {
{{ branchRules.length }}
</div>
</div>
<gl-disclosure-dropdown
v-if="glFeatures.addBranchRule"
:toggle-text="$options.i18n.addBranchRule"
:items="addRuleItems"
size="small"
no-caret
/>
<gl-button
v-else
v-gl-modal="$options.modalId"
size="small"
class="gl-ml-3"
@ -99,6 +191,37 @@ export default {
</div>
</ul>
<gl-modal
v-if="glFeatures.addBranchRule"
:ref="$options.modalId"
:modal-id="$options.modalId"
:title="$options.i18n.createBranchRule"
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary="addBranchRule"
@change="searchQuery = ''"
>
<gl-form-group
:label="$options.i18n.branchName"
:description="$options.i18n.branchNameDescription"
>
<gl-collapsible-listbox
v-model="branchRuleName"
searchable
:items="createRuleItems"
:toggle-text="createRuleText"
block
@search="handleBranchRuleSearch"
@select="selectBranchRuleName"
>
<template v-if="isWildcardAvailable" #list-item="{ item }">
{{ item.text }}
<code>{{ searchQuery }}</code>
</template>
</gl-collapsible-listbox>
</gl-form-group>
</gl-modal>
<gl-modal
v-else
:ref="$options.modalId"
:modal-id="$options.modalId"
:title="$options.i18n.addBranchRule"

View File

@ -15,6 +15,16 @@ export const I18N = {
'BranchRules|After a protected branch is created, it will show up in the list as a branch rule.',
),
createProtectedBranch: s__('BranchRules|Create protected branch'),
createBranchRule: s__('BranchRules|Create branch rule'),
branchName: s__('BranchRules|Branch name or pattern'),
branchNamePlaceholder: s__('BranchRules|Select Branch or create wildcard'),
branchNameDescription: s__(
'BranchRules|Wildcards such as *-stable or production/* are supported',
),
createBranchRuleError: s__('BranchRules|Something went wrong while creating branch rule.'),
createBranchRuleSuccess: s__('BranchRules|Branch rule created.'),
createWildcard: s__('BranchRules|Create wildcard'),
cancel: s__('BranchRules|Cancel'),
};
export const PROTECTED_BRANCHES_ANCHOR = '#js-protected-branches-settings';

View File

@ -0,0 +1,8 @@
mutation createBranchRule($projectPath: ID!, $name: String!) {
branchRuleCreate(input: { projectPath: $projectPath, name: $name }) {
errors
branchRule {
name
}
}
}

View File

@ -7,6 +7,10 @@ module Projects
before_action :authorize_admin_project!
before_action :define_variables, only: [:create_deploy_token]
before_action do
push_frontend_feature_flag(:add_branch_rule, @project)
end
feature_category :source_code_management, [:show, :cleanup, :update]
feature_category :continuous_delivery, [:create_deploy_token]
urgency :low, [:show, :create_deploy_token]

View File

@ -44,6 +44,7 @@ class ProjectsController < Projects::ApplicationController
push_frontend_feature_flag(:explain_code_chat, current_user)
push_frontend_feature_flag(:issue_email_participants, @project)
push_frontend_feature_flag(:encoding_logs_tree)
push_frontend_feature_flag(:add_branch_rule, @project)
# TODO: We need to remove the FF eventually when we rollout page_specific_styles
push_frontend_feature_flag(:page_specific_styles, current_user)
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)

View File

@ -0,0 +1,12 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Cloud Connector Access",
"type": "object",
"available_services": {
"type": "array",
"items": {
"type": "object"
}
},
"additionalProperties": true
}

View File

@ -0,0 +1,8 @@
---
name: add_branch_rule
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/435948
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/437003
milestone: '16.8'
type: development
group: group::source code
default_enabled: false

View File

@ -0,0 +1,8 @@
---
table_name: cloud_connector_access
feature_categories:
- cloud_connector
description: Information about Cloud Connector features
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140456
milestone: '16.8'
gitlab_schema: gitlab_main_clusterwide

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class CreateCloudConnectorAccess < Gitlab::Database::Migration[2.2]
enable_lock_retries!
milestone '16.8'
def change
create_table :cloud_connector_access do |t|
t.timestamps_with_timezone null: false
t.jsonb :data, null: false
end
end
end

View File

@ -0,0 +1 @@
1403f5bcc26c0f8b76f27b77305fc4089c87cf06362c3c9919a71dd3efcee492

View File

@ -14887,6 +14887,22 @@ CREATE SEQUENCE ci_variables_id_seq
ALTER SEQUENCE ci_variables_id_seq OWNED BY ci_variables.id;
CREATE TABLE cloud_connector_access (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
data jsonb NOT NULL
);
CREATE SEQUENCE cloud_connector_access_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE cloud_connector_access_id_seq OWNED BY cloud_connector_access.id;
CREATE TABLE cluster_agent_tokens (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
@ -26626,6 +26642,8 @@ ALTER TABLE ONLY ci_unit_tests ALTER COLUMN id SET DEFAULT nextval('ci_unit_test
ALTER TABLE ONLY ci_variables ALTER COLUMN id SET DEFAULT nextval('ci_variables_id_seq'::regclass);
ALTER TABLE ONLY cloud_connector_access ALTER COLUMN id SET DEFAULT nextval('cloud_connector_access_id_seq'::regclass);
ALTER TABLE ONLY cluster_agent_tokens ALTER COLUMN id SET DEFAULT nextval('cluster_agent_tokens_id_seq'::regclass);
ALTER TABLE ONLY cluster_agents ALTER COLUMN id SET DEFAULT nextval('cluster_agents_id_seq'::regclass);
@ -28637,6 +28655,9 @@ ALTER TABLE ONLY ci_unit_tests
ALTER TABLE ONLY ci_variables
ADD CONSTRAINT ci_variables_pkey PRIMARY KEY (id);
ALTER TABLE ONLY cloud_connector_access
ADD CONSTRAINT cloud_connector_access_pkey PRIMARY KEY (id);
ALTER TABLE ONLY cluster_agent_tokens
ADD CONSTRAINT cluster_agent_tokens_pkey PRIMARY KEY (id);

View File

@ -10,12 +10,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
You can organize GitLab [groups](../index.md) into subgroups. You can use subgroups to:
- Separate internal and external organizations. Because every subgroup can have its own
- Separate internal and external content. Because every subgroup can have its own
[visibility level](../../public_access.md), you can host groups for different
purposes under the same parent group.
- Organize large projects. You can use subgroups to give different access to parts of
- Organize large projects. You can use subgroups to manage who can access parts of
the source code.
- Manage people and control visibility. Give a user a different
- Manage permissions. Give a user a different
[role](../../permissions.md#group-members-permissions) for each group they're [a member of](#subgroup-membership).
Subgroups can:
@ -25,7 +25,7 @@ Subgroups can:
- Be nested up to 20 levels.
- Use [runners](../../../ci/runners/index.md) registered to parent groups:
- Secrets configured for the parent group are available to subgroup jobs.
- Users with the Maintainer role in projects that belong to subgroups can see the details of runners registered to
- Users with at least the Maintainer role in projects that belong to subgroups can see the details of runners registered to
parent groups.
For example:
@ -117,7 +117,7 @@ For more information, view the [permissions table](../../permissions.md#group-me
## Subgroup membership
When you add a member to a group, that member is also added to all subgroups of that group.
The member's permissions are inherited from the group's parent.
The member's permissions are inherited from the group into all subgroups.
Subgroup members can be:
@ -189,8 +189,8 @@ Members can be [filtered by inherited or direct membership](../index.md#filter-a
Users with the Owner role in a subgroup can add members to it.
You can't give a user a role in a subgroup that is lower than the roles the user has in ancestor groups.
To override a user's role in an ancestor group, add the user to the subgroup again with a higher role.
You can't give a user a role in a subgroup that is lower than the roles the user has in parent groups.
To override a user's role in a parent group, add the user to the subgroup again with a higher role.
For example:
- If User 1 is added to group _Two_ with the Developer role, User 1 inherits that role in every subgroup of group _Two_.
@ -201,7 +201,7 @@ For example:
## Mention subgroups
Mentioning subgroups ([`@<subgroup_name>`](../../discussions/index.md#mentions)) in issues, commits, and merge requests
Mentioning subgroups ([`@<subgroup_name>`](../../discussions/index.md#mentions)) in epics, issues, commits, and merge requests
notifies all direct members of that group. Inherited members of a subgroup are not notified by mentions.
Mentioning works the same as for projects and groups, and you can choose the group of members to be notified.

View File

@ -70,7 +70,7 @@ This specific Ruby image is maintained on [DockerHub](https://hub.docker.com/_/r
Edit your `.gitlab-ci.yml` file and add this text as the first line:
```yaml
image: ruby:2.7
image: ruby:3.2
```
If your SSG needs [NodeJS](https://nodejs.org/) to build, you must specify an
@ -156,7 +156,7 @@ pages:
Your `.gitlab-ci.yml` file should now look like this:
```yaml
image: ruby:2.7
image: ruby:3.2
pages:
script:
@ -198,7 +198,7 @@ First, add a `workflow` section to force the pipeline to run only when changes a
pushed to branches:
```yaml
image: ruby:2.7
image: ruby:3.2
workflow:
rules:
@ -218,7 +218,7 @@ Then configure the pipeline to run the job for the
[default branch](../../repository/branches/default.md) (here, `main`) only.
```yaml
image: ruby:2.7
image: ruby:3.2
workflow:
rules:
@ -249,7 +249,7 @@ To specify a stage for your job to run in,
add a `stage` line to your CI file:
```yaml
image: ruby:2.7
image: ruby:3.2
workflow:
rules:
@ -273,7 +273,7 @@ Now add another job to the CI file, telling it to
test every push to every branch **except** the `main` branch:
```yaml
image: ruby:2.7
image: ruby:3.2
workflow:
rules:
@ -325,7 +325,7 @@ for both jobs, `pages` and `test`.
Move these commands to a `before_script` section:
```yaml
image: ruby:2.7
image: ruby:3.2
workflow:
rules:
@ -366,7 +366,7 @@ This example caches Jekyll dependencies in a `vendor` directory
when you run `bundle install`:
```yaml
image: ruby:2.7
image: ruby:3.2
workflow:
rules:

View File

@ -8,7 +8,15 @@ module Gitlab
end
def current_application_settings
cached_application_settings
cached_application_settings || uncached_application_settings
end
def current_application_settings?
::ApplicationSetting.current.present?
end
def expire_current_application_settings
::ApplicationSetting.expire
end
private
@ -25,9 +33,48 @@ module Gitlab
end
end
def uncached_application_settings
return fake_application_settings if Gitlab::Runtime.rake? && !connect_to_db?
current_settings = ::ApplicationSetting.current
# If there are pending migrations, it's possible there are columns that
# need to be added to the application settings. To prevent Rake tasks
# and other callers from failing, use any loaded settings and return
# defaults for missing columns.
if Gitlab::Runtime.rake? && ::ApplicationSetting.connection.migration_context.needs_migration?
db_attributes = current_settings&.attributes || {}
fake_application_settings(db_attributes)
elsif current_settings.present?
current_settings
else
::ApplicationSetting.create_from_defaults
end
rescue ::ApplicationSetting::Recursion
in_memory_application_settings
end
def in_memory_application_settings
@in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults
end
def fake_application_settings(attributes = {})
Gitlab::FakeApplicationSettings.new(::ApplicationSetting.defaults.merge(attributes || {}))
end
def connect_to_db?
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
active_db_connection = begin
::ApplicationSetting.connection.active?
rescue StandardError
false
end
active_db_connection &&
ApplicationSetting.database.cached_table_exists?
rescue ActiveRecord::NoDatabaseError
false
end
end
end
end

View File

@ -12,22 +12,18 @@ module Gitlab
end
def current_application_settings
Gitlab::SafeRequestStore.fetch(:current_application_settings) { ensure_application_settings! }
Gitlab::SafeRequestStore.fetch(:current_application_settings) { Gitlab::ApplicationSettingFetcher.current_application_settings }
end
def current_application_settings?
Gitlab::SafeRequestStore.exist?(:current_application_settings) || ::ApplicationSetting.current.present?
Gitlab::SafeRequestStore.exist?(:current_application_settings) || Gitlab::ApplicationSettingFetcher.current_application_settings?
end
def expire_current_application_settings
::ApplicationSetting.expire
Gitlab::ApplicationSettingFetcher.expire_current_application_settings
Gitlab::SafeRequestStore.delete(:current_application_settings)
end
def clear_in_memory_application_settings!
@in_memory_application_settings = nil
end
def method_missing(name, *args, **kwargs, &block)
current_application_settings.send(name, *args, **kwargs, &block) # rubocop:disable GitlabSecurity/PublicSend
end
@ -35,54 +31,6 @@ module Gitlab
def respond_to_missing?(name, include_private = false)
current_application_settings.respond_to?(name, include_private) || super
end
private
def ensure_application_settings!
Gitlab::ApplicationSettingFetcher.current_application_settings || uncached_application_settings
end
def uncached_application_settings
return fake_application_settings if Gitlab::Runtime.rake? && !connect_to_db?
current_settings = ::ApplicationSetting.current
# If there are pending migrations, it's possible there are columns that
# need to be added to the application settings. To prevent Rake tasks
# and other callers from failing, use any loaded settings and return
# defaults for missing columns.
if Gitlab::Runtime.rake? && ::ApplicationSetting.connection.migration_context.needs_migration?
db_attributes = current_settings&.attributes || {}
fake_application_settings(db_attributes)
elsif current_settings.present?
current_settings
else
::ApplicationSetting.create_from_defaults
end
rescue ::ApplicationSetting::Recursion
in_memory_application_settings
end
def fake_application_settings(attributes = {})
Gitlab::FakeApplicationSettings.new(::ApplicationSetting.defaults.merge(attributes || {}))
end
def in_memory_application_settings
@in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults
end
def connect_to_db?
# When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised
active_db_connection = begin
::ApplicationSetting.connection.active?
rescue StandardError
false
end
active_db_connection &&
ApplicationSetting.database.cached_table_exists?
rescue ActiveRecord::NoDatabaseError
false
end
end
end
end

View File

@ -8663,15 +8663,27 @@ msgstr ""
msgid "BranchRules|Branch name or pattern"
msgstr ""
msgid "BranchRules|Branch rule created."
msgstr ""
msgid "BranchRules|Branch rules details"
msgstr ""
msgid "BranchRules|Cancel"
msgstr ""
msgid "BranchRules|Check for a status response in merge requests. Failures do not block merges. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "BranchRules|Create branch rule"
msgstr ""
msgid "BranchRules|Create protected branch"
msgstr ""
msgid "BranchRules|Create wildcard"
msgstr ""
msgid "BranchRules|Create wildcard: %{searchTerm}"
msgstr ""
@ -8729,6 +8741,12 @@ msgstr ""
msgid "BranchRules|Roles"
msgstr ""
msgid "BranchRules|Select Branch or create wildcard"
msgstr ""
msgid "BranchRules|Something went wrong while creating branch rule."
msgstr ""
msgid "BranchRules|Status checks"
msgstr ""
@ -8747,6 +8765,9 @@ msgstr ""
msgid "BranchRules|View details"
msgstr ""
msgid "BranchRules|Wildcards such as *-stable or production/* are supported"
msgstr ""
msgid "BranchRules|default"
msgstr ""

View File

@ -12,8 +12,8 @@ describe('CiResourceAbout', () => {
openMergeRequestsCount: 9,
latestVersion: {
id: 1,
tagName: 'v1.0.0',
tagPath: 'path/to/release',
name: 'v1.0.0',
path: 'path/to/release',
releasedAt: '2022-08-23T17:19:09Z',
},
webPath: 'path/to/project',

View File

@ -113,7 +113,7 @@ describe('CiResourceHeader', () => {
createComponent({
props: {
pipelineStatus: status,
latestVersion: { tagName: '1.0.0', tagPath: 'path/to/release' },
latestVersion: { name: '1.0.0', path: 'path/to/release' },
},
});
});

View File

@ -21,7 +21,7 @@ describe('CiResourcesListItem', () => {
const release = {
author: { name: 'author', webUrl: '/user/1' },
releasedAt: Date.now(),
tagName: '1.0.0',
name: '1.0.0',
};
const defaultProps = {
resource,
@ -114,7 +114,7 @@ describe('CiResourcesListItem', () => {
it('renders the version badge', () => {
expect(findBadge().exists()).toBe(true);
expect(findBadge().text()).toBe(release.tagName);
expect(findBadge().text()).toBe(release.name);
});
});
});

View File

@ -298,8 +298,8 @@ export const catalogSharedDataMock = {
latestVersion: {
__typename: 'Release',
id: '3',
tagName: '1.0.0',
tagPath: 'path/to/release',
name: '1.0.0',
path: 'path/to/release',
releasedAt: Date.now(),
author: { id: 1, webUrl: 'profile/1', name: 'username' },
},
@ -344,7 +344,7 @@ export const catalogAdditionalDetailsMock = {
],
},
},
tagName: 'v1.0.2',
name: 'v1.0.2',
releasedAt: '2022-08-23T17:19:09Z',
},
],
@ -366,8 +366,8 @@ const generateResourcesNodes = (count = 20, startId = 0) => {
latestVersion: {
__typename: 'Release',
id: '3',
tagName: '1.0.0',
tagPath: 'path/to/release',
name: '1.0.0',
path: 'path/to/release',
releasedAt: Date.now(),
author: { id: 1, webUrl: 'profile/1', name: 'username' },
},

View File

@ -1,16 +1,21 @@
import Vue from 'vue';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { GlModal } from '@gitlab/ui';
import { GlModal, GlCollapsibleListbox, GlDisclosureDropdown } from '@gitlab/ui';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BranchRules from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import createBranchRuleMutation from '~/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql';
import { createAlert } from '~/alert';
import {
branchRulesMockResponse,
appProvideMock,
createBranchRuleMockResponse,
} from 'ee_else_ce_jest/projects/settings/repository/branch_rules/mock_data';
import {
I18N,
@ -31,16 +36,33 @@ Vue.use(VueApollo);
describe('Branch rules app', () => {
let wrapper;
let fakeApollo;
const openBranches = [
{ text: 'branch1', id: 'branch1', title: 'branch1' },
{ text: 'branch2', id: 'branch2', title: 'branch2' },
];
const branchRulesQuerySuccessHandler = jest.fn().mockResolvedValue(branchRulesMockResponse);
const addRuleMutationSuccessHandler = jest.fn().mockResolvedValue(createBranchRuleMockResponse);
const createComponent = async ({ queryHandler = branchRulesQuerySuccessHandler } = {}) => {
fakeApollo = createMockApollo([[branchRulesQuery, queryHandler]]);
const createComponent = async ({
glFeatures = { addBranchRule: true },
queryHandler = branchRulesQuerySuccessHandler,
mutationHandler = addRuleMutationSuccessHandler,
} = {}) => {
fakeApollo = createMockApollo([
[branchRulesQuery, queryHandler],
[createBranchRuleMutation, mutationHandler],
]);
wrapper = mountExtended(BranchRules, {
apolloProvider: fakeApollo,
provide: appProvideMock,
stubs: { GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }) },
provide: {
...appProvideMock,
glFeatures,
},
stubs: {
GlDisclosureDropdown,
GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }),
},
directives: { GlModal: createMockDirective('gl-modal') },
});
@ -51,19 +73,18 @@ describe('Branch rules app', () => {
const findEmptyState = () => wrapper.findByTestId('empty');
const findAddBranchRuleButton = () => wrapper.findByRole('button', I18N.addBranchRule);
const findModal = () => wrapper.findComponent(GlModal);
const findAddBranchRuleDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findCreateBranchRuleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
beforeEach(() => {
window.gon = {
open_branches: openBranches,
};
setWindowLocation(TEST_HOST);
});
beforeEach(() => createComponent());
it('displays an error if branch rules query fails', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
expect(createAlert).toHaveBeenCalledWith({ message: I18N.queryError });
});
it('displays an empty state if no branch rules are present', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
expect(findEmptyState().text()).toBe(I18N.emptyState);
});
it('renders branch rules', () => {
const { nodes } = branchRulesMockResponse.data.project.branchRules;
@ -78,7 +99,75 @@ describe('Branch rules app', () => {
expect(findAllBranchRules().at(1).props('branchProtection')).toEqual(nodes[1].branchProtection);
});
it('displays an error if branch rules query fails', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
expect(createAlert).toHaveBeenCalledWith({ message: I18N.queryError });
});
it('displays an empty state if no branch rules are present', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
expect(findEmptyState().text()).toBe(I18N.emptyState);
});
describe('Add branch rule', () => {
it('renders an Add branch rule dropdown', () => {
expect(findAddBranchRuleDropdown().props('toggleText')).toBe('Add branch rule');
});
it('renders a modal with correct props/attributes', () => {
expect(findModal().props()).toMatchObject({
title: I18N.createBranchRule,
modalId: BRANCH_PROTECTION_MODAL_ID,
actionCancel: {
text: 'Create branch rule',
},
actionPrimary: {
attributes: {
disabled: true,
variant: 'confirm',
},
text: 'Create protected branch',
},
});
});
it('renders listbox with branch names', () => {
expect(findCreateBranchRuleListbox().exists()).toBe(true);
expect(findCreateBranchRuleListbox().props('items')).toHaveLength(openBranches.length);
expect(findCreateBranchRuleListbox().props('toggleText')).toBe(
'Select Branch or create wildcard',
);
});
it('when the primary modal action is clicked it calls create rule mutation', async () => {
findCreateBranchRuleListbox().vm.$emit('select', openBranches[0].text);
await nextTick();
findModal().vm.$emit('primary');
await nextTick();
await nextTick();
expect(addRuleMutationSuccessHandler).toHaveBeenCalledWith({
name: 'branch1',
projectPath: 'some/project/path',
});
});
it('shows alert when mutation fails', async () => {
createComponent({ mutationHandler: jest.fn().mockRejectedValue() });
findCreateBranchRuleListbox().vm.$emit('select', openBranches[0].text);
await nextTick();
findModal().vm.$emit('primary');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({
message: 'Something went wrong while creating branch rule.',
});
});
});
describe('Add branch rule when addBranchRule FF disabled', () => {
beforeEach(() => {
window.gon.open_branches = openBranches;
createComponent({ glFeatures: { addBranchRule: false } });
});
it('renders an Add branch rule button', () => {
expect(findAddBranchRuleButton().exists()).toBe(true);
});

View File

@ -65,8 +65,22 @@ export const branchRulesMockResponse = {
},
};
export const createBranchRuleMockResponse = {
data: {
branchRuleCreate: {
errors: [],
branchRule: {
name: '*dkd',
__typename: 'BranchRule',
},
__typename: 'BranchRuleCreatePayload',
},
},
};
export const appProvideMock = {
projectPath: 'some/project/path',
branchRulesPath: 'settings/repository/branch_rules',
};
export const branchRuleProvideMock = {

View File

@ -44,23 +44,181 @@ RSpec.describe Gitlab::ApplicationSettingFetcher, feature_category: :cell do
end
context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is false' do
let_it_be(:settings) { create(:application_setting) }
context 'and an error is raised' do
before do
allow(ApplicationSetting).to receive(:cached).and_raise(StandardError)
# The cached method is called twice:
# - ApplicationSettingFetcher
# - ApplicationSetting (CachedAttribute module)
# For this test, the first needs to raise an exception
# The second is swallowed on production so that should not raise an exception
# So we only let the first call raise an exception
# Alternatively, we could mock Rails.env.production? but I prefer not to
raise_exception = true
allow(ApplicationSetting).to receive(:cached).twice do
if raise_exception
raise_exception = false
raise(StandardError)
else
ApplicationSetting.last
end
end
end
it 'returns nil' do
expect(current_application_settings).to be_nil
it 'will retrieve uncached ApplicationSetting' do
expect(ApplicationSetting).to receive(:current).and_call_original
expect(current_application_settings).to eq(settings)
end
end
context 'and settings in cache' do
before do
# Warm the cache
ApplicationSetting.current
end
it 'fetches the settings from cache' do
expect(::ApplicationSetting).to receive(:cached).and_call_original
expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
expect(ActiveRecord::QueryRecorder.new { current_application_settings }.count).to eq(0)
end
end
context 'and settings are not in cache' do
before do
allow(ApplicationSetting).to receive(:cached).and_return(nil)
end
context 'and we are running a Rake task' do
before do
allow(Gitlab::Runtime).to receive(:rake?).and_return(true)
end
context 'and database does not exist' do
before do
allow(::ApplicationSetting.database)
.to receive(:cached_table_exists?).and_raise(ActiveRecord::NoDatabaseError)
end
it 'uses Gitlab::FakeApplicationSettings' do
expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
end
end
context 'and database connection is not active' do
before do
allow(::ApplicationSetting.connection).to receive(:active?).and_return(false)
end
it 'uses Gitlab::FakeApplicationSettings' do
expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
end
end
context 'and table does not exist' do
before do
allow(::ApplicationSetting.database).to receive(:cached_table_exists?).and_return(false)
end
it 'uses Gitlab::FakeApplicationSettings' do
expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
end
end
context 'and database connection raises some error' do
before do
allow(::ApplicationSetting.connection).to receive(:active?).and_raise(StandardError)
end
it 'uses Gitlab::FakeApplicationSettings' do
expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
end
end
context 'and there are pending database migrations' do
before do
allow_next_instance_of(ActiveRecord::MigrationContext) do |migration_context|
allow(migration_context).to receive(:needs_migration?).and_return(true)
end
end
it 'uses Gitlab::FakeApplicationSettings' do
expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
end
context 'when a new setting is used but the migration did not run yet' do
let(:default_attributes) { { new_column: 'some_value' } }
before do
allow(ApplicationSetting).to receive(:defaults).and_return(default_attributes)
end
it 'uses the default value if present' do
expect(current_application_settings.new_column).to eq(
default_attributes[:new_column]
)
end
end
end
end
context 'and settings are in database' do
it 'returns settings from database' do
expect(current_application_settings).to eq(settings)
end
end
context 'and settings are not in the database' do
before do
allow(ApplicationSetting).to receive(:current).and_return(nil)
end
it 'returns default settings' do
expect(ApplicationSetting).to receive(:create_from_defaults).and_call_original
expect(current_application_settings).to eq(settings)
end
end
context 'when we hit a recursive loop' do
before do
allow(ApplicationSetting).to receive(:current).and_raise(ApplicationSetting::Recursion)
end
it 'recovers and returns in-memory settings' do
settings = described_class.current_application_settings
expect(settings).to be_a(ApplicationSetting)
expect(settings).not_to be_persisted
end
end
end
end
end
describe '.expire_current_application_settings' do
subject(:expire) { described_class.expire_current_application_settings }
it 'expires ApplicationSetting' do
expect(ApplicationSetting).to receive(:expire)
expire
end
end
describe '.current_application_settings?' do
subject(:settings?) { described_class.current_application_settings? }
context 'when settings exist' do
let_it_be(:settings) { create(:application_setting) }
it { is_expected.to be(true) }
end
context 'when settings do not exist' do
it { is_expected.to be(false) }
end
end
end

View File

@ -102,193 +102,39 @@ RSpec.describe Gitlab::CurrentSettings, feature_category: :shared do
described_class.home_page_url
end
context 'in a Rake task with DB unavailable' do
before do
allow(Gitlab::Runtime).to receive(:rake?).and_return(true)
allow(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings).and_return(nil)
# For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
# during the initialization phase of the test suite, so instead let's mock the internals of it
allow(ApplicationSetting.connection).to receive(:active?).and_return(false)
end
context 'and no settings in cache' do
it 'returns a FakeApplicationSettings object' do
expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings)
end
it 'does not issue any query' do
expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
end
end
end
context 'with DB available' do
# This method returns the ::ApplicationSetting.defaults hash
# but with respect of custom attribute accessors of ApplicationSetting model
def settings_from_defaults
ar_wrapped_defaults = ::ApplicationSetting.build_from_defaults.attributes
ar_wrapped_defaults.slice(*::ApplicationSetting.defaults.keys)
end
context 'and settings in cache' do
include_context 'with settings in cache'
it 'fetches the settings from cache' do
# For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
# during the initialization phase of the test suite, so instead let's mock the internals of it
expect(ApplicationSetting.connection).not_to receive(:active?)
expect(ApplicationSetting.connection).not_to receive(:cached_table_exists?)
expect_any_instance_of(ActiveRecord::MigrationContext).not_to receive(:needs_migration?)
expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
end
end
context 'and no settings in cache' do
before do
allow(ApplicationSetting.connection).to receive(:active?).and_return(true)
allow(ApplicationSetting.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true)
end
context 'with RequestStore enabled', :request_store do
it 'fetches the settings from DB only once' do
described_class.current_application_settings # warm the cache
expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0)
end
end
it 'creates default ApplicationSettings if none are present' do
settings = described_class.current_application_settings
expect(settings).to be_a(ApplicationSetting)
expect(settings).to be_persisted
expect(settings).to have_attributes(settings_from_defaults)
end
context 'when we hit a recursive loop' do
before do
expect(ApplicationSetting).to receive(:create_from_defaults) do
raise ApplicationSetting::Recursion
end
end
it 'recovers and returns in-memory settings' do
settings = described_class.current_application_settings
expect(settings).to be_a(ApplicationSetting)
expect(settings).not_to be_persisted
end
end
context 'when ApplicationSettings does not have a primary key' do
before do
allow(ApplicationSetting.connection).to receive(:primary_key).with('application_settings').and_return(nil)
end
it 'raises an exception if ApplicationSettings does not have a primary key' do
expect { described_class.current_application_settings }.to raise_error(/table is missing a primary key constraint/)
end
end
context 'with pending migrations' do
let(:current_settings) { described_class.current_application_settings }
before do
allow(Gitlab::Runtime).to receive(:rake?).and_return(false)
end
shared_examples 'a non-persisted ApplicationSetting object' do
it 'uses the default value from ApplicationSetting.defaults' do
expect(current_settings.signup_enabled).to eq(ApplicationSetting.defaults[:signup_enabled])
end
it 'uses the default value from custom ApplicationSetting accessors' do
expect(current_settings.commit_email_hostname).to eq(ApplicationSetting.default_commit_email_hostname)
end
it 'responds to predicate methods' do
expect(current_settings.signup_enabled?).to eq(current_settings.signup_enabled)
end
end
context 'in a Rake task' do
before do
allow(Gitlab::Runtime).to receive(:rake?).and_return(true)
expect_any_instance_of(ActiveRecord::MigrationContext).to receive(:needs_migration?).and_return(true)
end
it_behaves_like 'a non-persisted ApplicationSetting object'
it 'returns a FakeApplicationSettings object' do
expect(current_settings).to be_a(Gitlab::FakeApplicationSettings)
end
context 'when a new column is used before being migrated' do
before do
allow(ApplicationSetting).to receive(:defaults).and_return({ foo: 'bar' })
end
it 'uses the default value if present' do
expect(current_settings.foo).to eq('bar')
end
end
end
context 'with no ApplicationSetting DB record' do
it_behaves_like 'a non-persisted ApplicationSetting object'
end
context 'with an existing ApplicationSetting DB record' do
before do
described_class.update!(home_page_url: 'http://mydomain.com')
end
it_behaves_like 'a non-persisted ApplicationSetting object'
it 'uses the value from the DB attribute if present and not overridden by an accessor' do
expect(current_settings.home_page_url).to eq('http://mydomain.com')
end
end
end
context 'when ApplicationSettings.current is present' do
it 'returns the existing application settings' do
expect(ApplicationSetting).to receive(:current).and_return(:current_settings)
expect(described_class.current_application_settings).to eq(:current_settings)
end
end
end
end
end
describe '#current_application_settings?', :use_clean_rails_memory_store_caching do
describe '#current_application_settings?' do
subject(:settings_set) { described_class.current_application_settings? }
before do
# unstub, it is stubbed in spec/spec_helper.rb
allow(described_class).to receive(:current_application_settings?).and_call_original
ApplicationSetting.delete_all # ensure no settings exist
end
it 'returns true when settings exist' do
described_class.update!(
home_page_url: 'http://mydomain.com',
signup_enabled: false)
context 'when settings are cached in RequestStore' do
before do
allow(Gitlab::SafeRequestStore).to receive(:exist?).with(:current_application_settings).and_return(true)
end
expect(described_class.current_application_settings?).to eq(true)
it 'returns true' do
expect(settings_set).to be(true)
end
end
it 'returns false when settings do not exist' do
expect(described_class.current_application_settings?).to eq(false)
context 'when ApplicationSettingFetcher.current_application_settings? returns true' do
before do
allow(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings?).and_return(true)
end
it 'returns true' do
expect(settings_set).to be(true)
end
end
context 'with cache', :request_store do
include_context 'with settings in cache'
it 'returns an in-memory ApplicationSetting object' do
expect(ApplicationSetting).not_to receive(:current)
expect(described_class.current_application_settings?).to eq(true)
context 'when not cached and not in ApplicationSettingFetcher' do
it 'returns false' do
expect(settings_set).to be(false)
end
end
end

View File

@ -420,7 +420,6 @@ RSpec.configure do |config|
config.after do
Fog.unmock! if Fog.mock?
Gitlab::CurrentSettings.clear_in_memory_application_settings!
Gitlab::ApplicationSettingFetcher.clear_in_memory_application_settings!
# Reset all feature flag stubs to default for testing

View File

@ -17,7 +17,6 @@ RSpec.configure do |config|
end
config.after(:context, :migration) do
Gitlab::CurrentSettings.clear_in_memory_application_settings!
Gitlab::ApplicationSettingFetcher.clear_in_memory_application_settings!
end