diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml
index 22df689a9cf..98bfa38d1fc 100644
--- a/.gitlab/ci/qa-common/main.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml
@@ -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
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
index b9d6173a777..929175b964f 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
@@ -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 }}
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
index 57d19af614f..42f8cea8727 100644
--- a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
@@ -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 {
{{ resource.name }}
- {{ tagName }}
+ {{ name }}
-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 }}
+
+
+
+
+ {{ item.text }}
+ {{ searchQuery }}
+
+
+
+
+ `](../../discussions/index.md#mentions)) in issues, commits, and merge requests
+Mentioning subgroups ([`@`](../../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.
diff --git a/doc/user/project/pages/getting_started/pages_from_scratch.md b/doc/user/project/pages/getting_started/pages_from_scratch.md
index 9de2703b82b..0695cc20ccd 100644
--- a/doc/user/project/pages/getting_started/pages_from_scratch.md
+++ b/doc/user/project/pages/getting_started/pages_from_scratch.md
@@ -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:
diff --git a/lib/gitlab/application_setting_fetcher.rb b/lib/gitlab/application_setting_fetcher.rb
index ec33a36f028..cc8f67dc541 100644
--- a/lib/gitlab/application_setting_fetcher.rb
+++ b/lib/gitlab/application_setting_fetcher.rb
@@ -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
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 9b8d9880434..64e0478734b 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 75fbc14023f..33368bfd4d5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js
index 658a135534b..1c791857df9 100644
--- a/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js
@@ -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',
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
index 6af9daabea0..b35c8a40744 100644
--- a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
@@ -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' },
},
});
});
diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
index d74b133f386..15add3f307f 100644
--- a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
+++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
@@ -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);
});
});
});
diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js
index e370ac5054f..9ca35943ae5 100644
--- a/spec/frontend/ci/catalog/mock.js
+++ b/spec/frontend/ci/catalog/mock.js
@@ -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' },
},
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index dd534bec25d..e86759ec6ca 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -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);
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
index d169397241d..5981647ce38 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -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 = {
diff --git a/spec/lib/gitlab/application_setting_fetcher_spec.rb b/spec/lib/gitlab/application_setting_fetcher_spec.rb
index 0cfb4eea2a3..0225a7608cb 100644
--- a/spec/lib/gitlab/application_setting_fetcher_spec.rb
+++ b/spec/lib/gitlab/application_setting_fetcher_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 9f666669c91..7fc50438c95 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -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
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index d5e301cb986..226e200fa25 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -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
diff --git a/spec/support/migration.rb b/spec/support/migration.rb
index d9db7ae52fe..dadcbb1941e 100644
--- a/spec/support/migration.rb
+++ b/spec/support/migration.rb
@@ -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