diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
index cc138ddcaf9523c1fd0038a126a5a927107d51bf..2ef281c818f45e5100b8e7186fd185717295e931 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -111,7 +111,14 @@ export default {
};
},
update({ project: { branchRules, group } }) {
- const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch);
+ const branchRule = branchRules.nodes.find((rule) => {
+ if (this.ruleId) {
+ // If we have a rule ID, match by both name and ID for precision
+ return rule.name === this.branch && getIdFromGraphQLId(rule.id) === this.ruleId;
+ }
+ // Fallback to name-only matching for backward compatibility
+ return rule.name === this.branch;
+ });
this.branchRule = branchRule;
this.branchProtection = branchRule?.branchProtection;
this.statusChecks = branchRule?.externalStatusChecks?.nodes || [];
@@ -149,8 +156,12 @@ export default {
},
},
data() {
+ const branchParam = getParameterByName(BRANCH_PARAM_NAME);
+ const ruleId = getParameterByName('id');
+
return {
- branch: getParameterByName(BRANCH_PARAM_NAME),
+ branch: branchParam,
+ ruleId,
branchProtection: {},
statusChecks: [],
branchRule: {},
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index 5aeabbace7e22bf199bc2d409d6867efcdfae93d..9c54f4d56bfddb537a816a3f445873f3c49db0b9 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -188,6 +188,7 @@ export default {
({
- getParameterByName: jest.fn().mockReturnValue('main'),
+ getParameterByName: jest.fn((param) => {
+ if (param === 'branch') return 'main';
+ if (param === 'id') return null;
+ return null;
+ }),
mergeUrlParams: jest.fn().mockReturnValue('/branches?state=all&search=%5Emain%24'),
joinPaths: jest.fn(),
setUrlParams: jest
@@ -194,7 +198,11 @@ describe('View branch rules', () => {
`(
'expectedIsEditAvailable: $expectedIsEditAvailable when $description',
async ({ allowEditSquashSetting, branch, expectedIsEditAvailable }) => {
- jest.spyOn(util, 'getParameterByName').mockReturnValueOnce(branch);
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return branch;
+ if (param === 'id') return null;
+ return null;
+ });
await createComponent({
canAdminProtectedBranches: true,
@@ -306,7 +314,11 @@ describe('View branch rules', () => {
});
it('does not render squash settings for wildcard branch rules', async () => {
- jest.spyOn(util, 'getParameterByName').mockReturnValueOnce('*');
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return '*';
+ if (param === 'id') return null;
+ return null;
+ });
await createComponent();
expect(findSquashSettingSection().exists()).toBe(false);
@@ -319,7 +331,11 @@ describe('View branch rules', () => {
${'All protected branches'} | ${false} | ${'hides squash section for protected branches'}
${'feature-*'} | ${false} | ${'hides squash section for wildcard branches'}
`('$description', async ({ branch, expectedExists }) => {
- jest.spyOn(util, 'getParameterByName').mockReturnValueOnce(branch);
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return branch;
+ if (param === 'id') return null;
+ return null;
+ });
await createComponent();
@@ -338,8 +354,62 @@ describe('View branch rules', () => {
expect(findBranchName().text()).toBe('main');
});
+ describe('parseBranchParam', () => {
+ it('gets branch and id from separate URL parameters', async () => {
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return 'feature-branch';
+ if (param === 'id') return '123';
+ return null;
+ });
+ await createComponent();
+
+ expect(wrapper.vm.branch).toBe('feature-branch');
+ expect(wrapper.vm.ruleId).toBe('123');
+ });
+
+ it('handles missing id parameter', async () => {
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return 'feature-branch';
+ if (param === 'id') return null;
+ return null;
+ });
+ await createComponent();
+
+ expect(wrapper.vm.branch).toBe('feature-branch');
+ expect(wrapper.vm.ruleId).toBe(null);
+ });
+
+ it('handles missing branch parameter', async () => {
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return null;
+ if (param === 'id') return '123';
+ return null;
+ });
+ await createComponent();
+
+ expect(wrapper.vm.branch).toBe(null);
+ expect(wrapper.vm.ruleId).toBe('123');
+ });
+
+ it('handles both parameters missing', async () => {
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return null;
+ if (param === 'id') return null;
+ return null;
+ });
+ await createComponent();
+
+ expect(wrapper.vm.branch).toBe(null);
+ expect(wrapper.vm.ruleId).toBe(null);
+ });
+ });
+
it('renders the correct label if all branches are targeted with wildcard', async () => {
- jest.spyOn(util, 'getParameterByName').mockReturnValueOnce(ALL_BRANCHES_WILDCARD);
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return ALL_BRANCHES_WILDCARD;
+ if (param === 'id') return null;
+ return null;
+ });
await createComponent();
expect(findAllBranches().text()).toBe('*');
@@ -512,7 +582,11 @@ describe('View branch rules', () => {
});
it('does not render delete rule button when target is All branches', () => {
- jest.spyOn(util, 'getParameterByName').mockReturnValueOnce('All branches');
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return 'All branches';
+ if (param === 'id') return null;
+ return null;
+ });
createComponent();
expect(findDeleteRuleButton().exists()).toBe(false);
@@ -583,7 +657,11 @@ describe('View branch rules', () => {
describe('When rendered for predefined rules', () => {
beforeEach(async () => {
- jest.spyOn(util, 'getParameterByName').mockReturnValueOnce('All branches');
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return 'All branches';
+ if (param === 'id') return null;
+ return null;
+ });
await createComponent({
glFeatures: { editBranchRules: true },
@@ -714,7 +792,11 @@ describe('View branch rules', () => {
describe('When rendered for a non-existing rule', () => {
beforeEach(async () => {
- jest.spyOn(util, 'getParameterByName').mockReturnValueOnce('non-existing-rule');
+ jest.spyOn(util, 'getParameterByName').mockImplementation((param) => {
+ if (param === 'branch') return 'non-existing-rule';
+ if (param === 'id') return null;
+ return null;
+ });
await createComponent({ glFeatures: { editBranchRules: true } });
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
index e2c486708ff26e3db770eb095e53e2d958f95bfd..a700d02377413f417abcfe8dd9d79b6a41e94e67 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
@@ -84,9 +84,10 @@ describe('Branch rule', () => {
it('renders a detail button with the correct href', () => {
const encodedBranchName = encodeURIComponent(branchRulePropsMock.name);
+ const ruleId = '3'; // Extracted from 'gid://gitlab/Projects::BranchRule/3'
expect(findDetailsButton().attributes('href')).toBe(
- `${branchRuleProvideMock.branchRulesPath}?branch=${encodedBranchName}`,
+ `${branchRuleProvideMock.branchRulesPath}?branch=${encodedBranchName}&id=${ruleId}`,
);
});
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 c1048955b347bbfa73daaf0afa6eef5206a9af57..81e53e6ffe4c80c51302c18bbc4e9b80df093478 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -28,7 +28,7 @@ export const squashOptionMockResponse = {
{
__typename: 'BranchRule',
name: 'main',
- id: 'gid://gitlab/Projects/BranchRule/1',
+ id: 'gid://gitlab/Projects::BranchRule/1',
squashOption: {
option: 'Encourage',
helpText: 'Checkbox is visible and selected by default.',
@@ -38,13 +38,13 @@ export const squashOptionMockResponse = {
{
__typename: 'BranchRule',
name: '*',
- id: 'gid://gitlab/Projects/BranchRule/2',
+ id: 'gid://gitlab/Projects::BranchRule/2',
squashOption: null,
},
{
__typename: 'BranchRule',
name: 'branch-with-$speci@l-#-chars',
- id: 'gid://gitlab/Projects/BranchRule/3',
+ id: 'gid://gitlab/Projects::BranchRule/3',
squashOption: {
option: 'Encourage',
helpText: 'Checkbox is visible and selected by default.',
@@ -67,7 +67,7 @@ export const branchRulesMockResponse = {
nodes: [
{
name: 'main',
- id: 'gid://gitlab/Projects/BranchRule/1',
+ id: 'gid://gitlab/Projects::BranchRule/1',
isDefault: true,
matchingBranchesCount: 1,
branchProtection: {
@@ -85,7 +85,7 @@ export const branchRulesMockResponse = {
},
{
name: 'test-*',
- id: 'gid://gitlab/Projects/BranchRule/2',
+ id: 'gid://gitlab/Projects::BranchRule/2',
isDefault: false,
matchingBranchesCount: 2,
branchProtection: {
@@ -161,6 +161,7 @@ export const branchRuleProvideMock = {
export const branchRulePropsMock = {
name: 'branch-with-$speci@l-#-chars',
+ id: 'gid://gitlab/Projects::BranchRule/3',
isDefault: true,
matchingBranchesCount: 1,
branchProtection: {
@@ -177,6 +178,7 @@ export const branchRulePropsMock = {
export const branchRuleWithoutDetailsPropsMock = {
name: 'branch-1',
+ id: 'gid://gitlab/Projects::BranchRule/4',
isDefault: false,
matchingBranchesCount: 1,
branchProtection: null,