Commit 749168c2 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '118825-Allow-skipping-CI-when-rebasing-in-UI' into 'master'

Allow skipping CI when rebasing in UI

See merge request gitlab-org/gitlab!76056
parents adffe5c2 8d50d4d1
<script>
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { GlSkeletonLoader } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import rebaseQuery from '../../queries/states/rebase.query.graphql';
import statusIcon from '../mr_widget_status_icon.vue';
import { REBASE_BUTTON_KEY, REBASE_WITHOUT_CI_BUTTON_KEY } from '../../constants';
export default {
name: 'MRWidgetRebase',
......@@ -25,8 +27,8 @@ export default {
},
components: {
statusIcon,
GlButton,
GlSkeletonLoader,
ActionsButton,
},
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: {
......@@ -44,6 +46,7 @@ export default {
state: {},
isMakingRequest: false,
rebasingError: null,
selectedRebaseAction: REBASE_BUTTON_KEY,
};
},
computed: {
......@@ -86,14 +89,36 @@ export default {
fastForwardMergeText() {
return __('Merge blocked: the source branch must be rebased onto the target branch.');
},
actions() {
return [this.rebaseAction, this.rebaseWithoutCiAction].filter((action) => action);
},
rebaseAction() {
return {
key: REBASE_BUTTON_KEY,
text: __('Rebase'),
secondaryText: __('Rebases and triggers a pipeline'),
attrs: {
'data-qa-selector': 'mr_rebase_button',
},
handle: () => this.rebase(),
};
},
rebaseWithoutCiAction() {
return {
key: REBASE_WITHOUT_CI_BUTTON_KEY,
text: __('Rebase without CI'),
secondaryText: __('Performs a rebase but skips triggering a new pipeline'),
handle: () => this.rebase({ skipCi: true }),
};
},
},
methods: {
rebase() {
rebase({ skipCi = false } = {}) {
this.isMakingRequest = true;
this.rebasingError = null;
this.service
.rebase()
.rebase({ skipCi })
.then(() => {
simplePoll(this.checkRebaseStatus);
})
......@@ -109,6 +134,9 @@ export default {
}
});
},
selectRebaseAction(key) {
this.selectedRebaseAction = key;
},
checkRebaseStatus(continuePolling, stopPolling) {
this.service
.poll()
......@@ -168,15 +196,14 @@ export default {
v-if="!rebaseInProgress && canPushToSourceBranch && !isMakingRequest"
class="accept-merge-holder clearfix js-toggle-container accept-action media space-children"
>
<gl-button
<actions-button
v-if="!glFeatures.restructuredMrWidget"
:loading="isMakingRequest"
:actions="actions"
:selected-key="selectedRebaseAction"
variant="confirm"
data-qa-selector="mr_rebase_button"
@click="rebase"
>
{{ __('Rebase') }}
</gl-button>
category="primary"
@select="selectRebaseAction"
/>
<span
v-if="!rebasingError"
:class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }"
......
......@@ -162,3 +162,6 @@ export const EXTENSION_SUMMARY_FAILED_CLASS = 'gl-text-red-500';
export const EXTENSION_SUMMARY_NEUTRAL_CLASS = 'gl-text-gray-700';
export { STATE_MACHINE };
export const REBASE_BUTTON_KEY = 'rebase';
export const REBASE_WITHOUT_CI_BUTTON_KEY = 'rebaseWithoutCi';
......@@ -55,8 +55,9 @@ export default class MRWidgetService {
return axios.get(this.endpoints.mergeActionsContentPath);
}
rebase() {
return axios.post(this.endpoints.rebasePath);
rebase({ skipCi = false } = {}) {
const path = `${this.endpoints.rebasePath}?skip_ci=${Boolean(skipCi)}`;
return axios.post(path);
}
fetchApprovals() {
......
......@@ -38,9 +38,12 @@ Now, when you visit the merge request page, you can accept it
If a fast-forward merge is not possible but a conflict free rebase is possible,
a rebase button is offered.
You can also rebase without running a CI/CD pipeline.
[Introduced in](https://gitlab.com/gitlab-org/gitlab/-/issues/118825) GitLab 14.7.
The rebase action is also available as a [quick action command: `/rebase`](../../../topics/git/git_rebase.md#rebase-from-the-gitlab-ui).
![Fast forward merge request](img/ff_merge_rebase.png)
![Fast forward merge request](img/ff_merge_rebase_v14_7.png)
If the target branch is ahead of the source branch and a conflict free rebase is
not possible, you need to rebase the
......
......@@ -25846,6 +25846,9 @@ msgstr ""
msgid "PerformanceBar|wall"
msgstr ""
msgid "Performs a rebase but skips triggering a new pipeline"
msgstr ""
msgid "Period in seconds"
msgstr ""
......@@ -29227,6 +29230,12 @@ msgstr ""
msgid "Rebase source branch on the target branch."
msgstr ""
msgid "Rebase without CI"
msgstr ""
msgid "Rebases and triggers a pipeline"
msgstr ""
msgid "Recaptcha verified?"
msgstr ""
......
......@@ -2,10 +2,15 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import WidgetRebase from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import {
REBASE_BUTTON_KEY,
REBASE_WITHOUT_CI_BUTTON_KEY,
} from '~/vue_merge_request_widget/constants';
let wrapper;
function factory(propsData, mergeRequestWidgetGraphql) {
function createWrapper(propsData, mergeRequestWidgetGraphql) {
wrapper = shallowMount(WidgetRebase, {
propsData,
data() {
......@@ -31,8 +36,9 @@ function factory(propsData, mergeRequestWidgetGraphql) {
}
describe('Merge request widget rebase component', () => {
const findRebaseMessageEl = () => wrapper.find('[data-testid="rebase-message"]');
const findRebaseMessageElText = () => findRebaseMessageEl().text();
const findRebaseMessage = () => wrapper.find('[data-testid="rebase-message"]');
const findRebaseMessageText = () => findRebaseMessage().text();
const findRebaseButton = () => wrapper.find(ActionsButton);
afterEach(() => {
wrapper.destroy();
......@@ -40,10 +46,10 @@ describe('Merge request widget rebase component', () => {
});
[true, false].forEach((mergeRequestWidgetGraphql) => {
describe(`widget graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => {
describe('While rebasing', () => {
describe(`widget graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'disabled'}`, () => {
describe('while rebasing', () => {
it('should show progress message', () => {
factory(
createWrapper(
{
mr: { rebaseInProgress: true },
service: {},
......@@ -51,24 +57,32 @@ describe('Merge request widget rebase component', () => {
mergeRequestWidgetGraphql,
);
expect(findRebaseMessageElText()).toContain('Rebase in progress');
expect(findRebaseMessageText()).toContain('Rebase in progress');
});
});
describe('With permissions', () => {
it('it should render rebase button and warning message', () => {
factory(
describe('with permissions', () => {
const rebaseMock = jest.fn().mockResolvedValue();
const pollMock = jest.fn().mockResolvedValue({});
beforeEach(() => {
createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: true,
},
service: {},
service: {
rebase: rebaseMock,
poll: pollMock,
},
},
mergeRequestWidgetGraphql,
);
});
const text = findRebaseMessageElText();
it('renders the warning message', () => {
const text = findRebaseMessageText();
expect(text).toContain('Merge blocked');
expect(text.replace(/\s\s+/g, ' ')).toContain(
......@@ -76,42 +90,79 @@ describe('Merge request widget rebase component', () => {
);
});
it('it should render error message when it fails', async () => {
factory(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: true,
},
service: {},
},
mergeRequestWidgetGraphql,
);
it('renders an error message when rebasing has failed', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ rebasingError: 'Something went wrong!' });
await nextTick();
expect(findRebaseMessageElText()).toContain('Something went wrong!');
expect(findRebaseMessageText()).toContain('Something went wrong!');
});
describe('"Rebase" button', () => {
it('is rendered', () => {
expect(findRebaseButton().exists()).toBe(true);
});
it('has rebase and rebase without CI actions', () => {
const actionNames = findRebaseButton()
.props('actions')
.map((action) => action.key);
expect(actionNames).toStrictEqual([REBASE_BUTTON_KEY, REBASE_WITHOUT_CI_BUTTON_KEY]);
});
it('defaults to rebase action', () => {
expect(findRebaseButton().props('selectedKey')).toStrictEqual(REBASE_BUTTON_KEY);
});
it('starts the rebase when clicking', async () => {
// ActionButtons use the actions props instead of emitting
// a click event, therefore simulating the behavior here:
findRebaseButton()
.props('actions')
.find((x) => x.key === REBASE_BUTTON_KEY)
.handle();
await nextTick();
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: false });
});
it('starts the CI-skipping rebase when clicking on "Rebase without CI"', async () => {
// ActionButtons use the actions props instead of emitting
// a click event, therefore simulating the behavior here:
findRebaseButton()
.props('actions')
.find((x) => x.key === REBASE_WITHOUT_CI_BUTTON_KEY)
.handle();
await nextTick();
expect(rebaseMock).toHaveBeenCalledWith({ skipCi: true });
});
});
});
describe('Without permissions', () => {
it('should render a message explaining user does not have permissions', () => {
factory(
describe('without permissions', () => {
const exampleTargetBranch = 'fake-branch-to-test-with';
beforeEach(() => {
createWrapper(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: false,
targetBranch: 'foo',
targetBranch: exampleTargetBranch,
},
service: {},
},
mergeRequestWidgetGraphql,
);
});
const text = findRebaseMessageElText();
it('renders a message explaining user does not have permissions', () => {
const text = findRebaseMessageText();
expect(text).toContain(
'Merge blocked: the source branch must be rebased onto the target branch.',
......@@ -119,32 +170,23 @@ describe('Merge request widget rebase component', () => {
expect(text).toContain('the source branch must be rebased');
});
it('should render the correct target branch name', () => {
const targetBranch = 'fake-branch-to-test-with';
factory(
{
mr: {
rebaseInProgress: false,
canPushToSourceBranch: false,
targetBranch,
},
service: {},
},
mergeRequestWidgetGraphql,
);
const elem = findRebaseMessageEl();
it('renders the correct target branch name', () => {
const elem = findRebaseMessage();
expect(elem.text()).toContain(
`Merge blocked: the source branch must be rebased onto the target branch.`,
);
});
it('does not render the "Rebase" button', () => {
expect(findRebaseButton().exists()).toBe(false);
});
});
describe('methods', () => {
it('checkRebaseStatus', async () => {
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
factory(
createWrapper(
{
mr: {},
service: {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment