Commit b005b8e8 authored by Piotr Stankowski's avatar Piotr Stankowski Committed by Paul Slaughter

MR widget: update merge commit message when default changed

Changelog: added
parent 65bb9b20
...@@ -82,6 +82,13 @@ export default { ...@@ -82,6 +82,13 @@ export default {
}; };
this.loading = false; this.loading = false;
if (!this.commitMessageIsTouched) {
this.commitMessage = this.state.defaultMergeCommitMessage;
}
if (!this.squashCommitMessageIsTouched) {
this.squashCommitMessage = this.state.defaultSquashCommitMessage;
}
if (this.state.mergeTrainsCount !== null && this.state.mergeTrainsCount !== undefined) { if (this.state.mergeTrainsCount !== null && this.state.mergeTrainsCount !== undefined) {
this.initPolling(); this.initPolling();
} }
...@@ -133,9 +140,11 @@ export default { ...@@ -133,9 +140,11 @@ export default {
isMakingRequest: false, isMakingRequest: false,
isMergingImmediately: false, isMergingImmediately: false,
commitMessage: this.mr.commitMessage, commitMessage: this.mr.commitMessage,
commitMessageIsTouched: false,
squashBeforeMerge: this.mr.squashIsSelected, squashBeforeMerge: this.mr.squashIsSelected,
isSquashReadOnly: this.mr.squashIsReadonly, isSquashReadOnly: this.mr.squashIsReadonly,
squashCommitMessage: this.mr.squashCommitMessage, squashCommitMessage: this.mr.squashCommitMessage,
squashCommitMessageIsTouched: false,
isPipelineFailedModalVisibleMergeTrain: false, isPipelineFailedModalVisibleMergeTrain: false,
isPipelineFailedModalVisibleNormalMerge: false, isPipelineFailedModalVisibleNormalMerge: false,
editCommitMessage: false, editCommitMessage: false,
...@@ -465,6 +474,14 @@ export default { ...@@ -465,6 +474,14 @@ export default {
}); });
}); });
}, },
setCommitMessage(val) {
this.commitMessage = val;
this.commitMessageIsTouched = true;
},
setSquashCommitMessage(val) {
this.squashCommitMessage = val;
this.squashCommitMessageIsTouched = true;
},
}, },
i18n: { i18n: {
mergeCommitTemplateHintText: s__( mergeCommitTemplateHintText: s__(
...@@ -630,21 +647,23 @@ export default { ...@@ -630,21 +647,23 @@ export default {
> >
<commit-edit <commit-edit
v-if="shouldShowSquashEdit" v-if="shouldShowSquashEdit"
v-model="squashCommitMessage" :value="squashCommitMessage"
:label="__('Squash commit message')" :label="__('Squash commit message')"
input-id="squash-message-edit" input-id="squash-message-edit"
class="gl-m-0! gl-p-0!" class="gl-m-0! gl-p-0!"
@input="setSquashCommitMessage"
> >
<template #header> <template #header>
<commit-message-dropdown v-model="squashCommitMessage" :commits="commits" /> <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" />
</template> </template>
</commit-edit> </commit-edit>
<commit-edit <commit-edit
v-if="shouldShowMergeEdit" v-if="shouldShowMergeEdit"
v-model="commitMessage" :value="commitMessage"
:label="__('Merge commit message')" :label="__('Merge commit message')"
input-id="merge-message-edit" input-id="merge-message-edit"
class="gl-m-0! gl-p-0!" class="gl-m-0! gl-p-0!"
@input="setCommitMessage"
/> />
<li class="gl-m-0! gl-p-0!"> <li class="gl-m-0! gl-p-0!">
<p class="form-text text-muted"> <p class="form-text text-muted">
...@@ -748,20 +767,22 @@ export default { ...@@ -748,20 +767,22 @@ export default {
<ul class="border-top content-list commits-list flex-list"> <ul class="border-top content-list commits-list flex-list">
<commit-edit <commit-edit
v-if="shouldShowSquashEdit" v-if="shouldShowSquashEdit"
v-model="squashCommitMessage" :value="squashCommitMessage"
:label="__('Squash commit message')" :label="__('Squash commit message')"
input-id="squash-message-edit" input-id="squash-message-edit"
squash squash
@input="setSquashCommitMessage"
> >
<template #header> <template #header>
<commit-message-dropdown v-model="squashCommitMessage" :commits="commits" /> <commit-message-dropdown :commits="commits" @input="setSquashCommitMessage" />
</template> </template>
</commit-edit> </commit-edit>
<commit-edit <commit-edit
v-if="shouldShowMergeEdit" v-if="shouldShowMergeEdit"
v-model="commitMessage" :value="commitMessage"
:label="__('Merge commit message')" :label="__('Merge commit message')"
input-id="merge-message-edit" input-id="merge-message-edit"
@input="setCommitMessage"
/> />
<li> <li>
<p class="form-text text-muted"> <p class="form-text text-muted">
......
fragment ReadyToMerge on Project { fragment ReadyToMerge on Project {
__typename
id id
onlyAllowMergeIfPipelineSucceeds onlyAllowMergeIfPipelineSucceeds
mergeRequestsFfOnlyEnabled mergeRequestsFfOnlyEnabled
squashReadOnly squashReadOnly
mergeRequest(iid: $iid) { mergeRequest(iid: $iid) {
__typename
id id
autoMergeEnabled autoMergeEnabled
shouldRemoveSourceBranch shouldRemoveSourceBranch
......
...@@ -84,7 +84,7 @@ Commit message templates support these variables: ...@@ -84,7 +84,7 @@ Commit message templates support these variables:
| `%{first_commit}` | Full message of the first commit in merge request diff. | `Update README.md` | | `%{first_commit}` | Full message of the first commit in merge request diff. | `Update README.md` |
| `%{first_multiline_commit}` | Full message of the first commit that's not a merge commit and has more than one line in message body. Merge request title if all commits aren't multiline. | `Update README.md`<br><br>`Improved project description in readme file.` | | `%{first_multiline_commit}` | Full message of the first commit that's not a merge commit and has more than one line in message body. Merge request title if all commits aren't multiline. | `Update README.md`<br><br>`Improved project description in readme file.` |
| `%{url}` | Full URL to the merge request. | `https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1` | | `%{url}` | Full URL to the merge request. | `https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1` |
| `%{approved_by}` | Line-separated list of the merge request approvers. This value is not updated until the first page refresh after an approval. | `Approved-by: Sidney Jones <sjones@example.com>` <br> `Approved-by: Zhang Wei <zwei@example.com>` | | `%{approved_by}` | Line-separated list of the merge request approvers. | `Approved-by: Sidney Jones <sjones@example.com>` <br> `Approved-by: Zhang Wei <zwei@example.com>` |
| `%{merged_by}` | User who merged the merge request. | `Alex Garcia <agarcia@example.com>` | | `%{merged_by}` | User who merged the merge request. | `Alex Garcia <agarcia@example.com>` |
| `%{co_authored_by}` | Names and emails of commit authors in a `Co-authored-by` Git commit trailer format. Limited to authors of 100 most recent commits in merge request. | `Co-authored-by: Zane Doe <zdoe@example.com>` <br> `Co-authored-by: Blake Smith <bsmith@example.com>` | | `%{co_authored_by}` | Names and emails of commit authors in a `Co-authored-by` Git commit trailer format. Limited to authors of 100 most recent commits in merge request. | `Co-authored-by: Zane Doe <zdoe@example.com>` <br> `Co-authored-by: Blake Smith <bsmith@example.com>` |
| `%{all_commits}` | Messages from all commits in the merge request. Limited to 100 most recent commits. Skips commit bodies exceeding 100KiB and merge commit messages. | `* Feature introduced` <br><br> `This commit implements feature` <br> `Changelog:added` <br><br> `* Bug fixed` <br><br> `* Documentation improved` <br><br>`This commit introduced better docs.`| | `%{all_commits}` | Messages from all commits in the merge request. Limited to 100 most recent commits. Skips commit bodies exceeding 100KiB and merge commit messages. | `* Feature introduced` <br><br> `This commit implements feature` <br> `Changelog:added` <br><br> `* Bug fixed` <br><br> `* Documentation improved` <br><br>`This commit introduced better docs.`|
...@@ -92,6 +92,10 @@ Commit message templates support these variables: ...@@ -92,6 +92,10 @@ Commit message templates support these variables:
Any line containing only an empty variable is removed. If the line to be removed is both Any line containing only an empty variable is removed. If the line to be removed is both
preceded and followed by an empty line, the preceding empty line is also removed. preceded and followed by an empty line, the preceding empty line is also removed.
After you edit a commit message on an open merge request, GitLab will
not automatically update the commit message again.
To restore the commit message to the project template, reload the page.
## Related topics ## Related topics
- [Squash and merge](squash_and_merge.md). - [Squash and merge](squash_and_merge.md).
...@@ -130,6 +130,25 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: ...@@ -130,6 +130,25 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
expect(response).to be_successful expect(response).to be_successful
end end
describe GraphQL::Query, type: :request do
include ApiHelpers
include GraphqlHelpers
context 'merge request in state readyToMerge query' do
base_input_path = 'vue_merge_request_widget/queries/states/'
base_output_path = 'graphql/merge_requests/states/'
query_name = 'ready_to_merge.query.graphql'
it "#{base_output_path}#{query_name}.json" do
query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: true)
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
expect_graphql_errors_to_be_empty
end
end
end
private private
def render_discussions_json(merge_request) def render_discussions_json(merge_request)
......
import { shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { GlSprintf } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import produce from 'immer';
import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import simplePoll from '~/lib/utils/simple_poll'; import simplePoll from '~/lib/utils/simple_poll';
import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue'; import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
...@@ -19,9 +24,11 @@ jest.mock('~/commons/nav/user_merge_requests', () => ({ ...@@ -19,9 +24,11 @@ jest.mock('~/commons/nav/user_merge_requests', () => ({
refreshUserMergeRequestCounts: jest.fn(), refreshUserMergeRequestCounts: jest.fn(),
})); }));
const commitMessage = 'This is the commit message'; const commitMessage = readyToMergeResponse.data.project.mergeRequest.defaultMergeCommitMessage;
const squashCommitMessage = 'This is the squash commit message'; const squashCommitMessage =
const commitMessageWithDescription = 'This is the commit message description'; readyToMergeResponse.data.project.mergeRequest.defaultSquashCommitMessage;
const commitMessageWithDescription =
readyToMergeResponse.data.project.mergeRequest.defaultMergeCommitMessageWithDescription;
const createTestMr = (customConfig) => { const createTestMr = (customConfig) => {
const mr = { const mr = {
isPipelineActive: false, isPipelineActive: false,
...@@ -42,6 +49,8 @@ const createTestMr = (customConfig) => { ...@@ -42,6 +49,8 @@ const createTestMr = (customConfig) => {
commitMessage, commitMessage,
squashCommitMessage, squashCommitMessage,
commitMessageWithDescription, commitMessageWithDescription,
defaultMergeCommitMessage: commitMessage,
defaultSquashCommitMessage: squashCommitMessage,
shouldRemoveSourceBranch: true, shouldRemoveSourceBranch: true,
canRemoveSourceBranch: false, canRemoveSourceBranch: false,
targetBranch: 'main', targetBranch: 'main',
...@@ -61,15 +70,25 @@ const createTestService = () => ({ ...@@ -61,15 +70,25 @@ const createTestService = () => ({
merge: jest.fn(), merge: jest.fn(),
poll: jest.fn().mockResolvedValue(), poll: jest.fn().mockResolvedValue(),
}); });
const localVue = createLocalVue();
localVue.use(VueApollo);
let wrapper; let wrapper;
let readyToMergeResponseSpy;
const findMergeButton = () => wrapper.find('[data-testid="merge-button"]'); const findMergeButton = () => wrapper.find('[data-testid="merge-button"]');
const findPipelineFailedConfirmModal = () => const findPipelineFailedConfirmModal = () =>
wrapper.findComponent(MergeFailedPipelineConfirmationDialog); wrapper.findComponent(MergeFailedPipelineConfirmationDialog);
const createReadyToMergeResponse = (customMr) => {
return produce(readyToMergeResponse, (draft) => {
Object.assign(draft.data.project.mergeRequest, customMr);
});
};
const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) => { const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) => {
wrapper = shallowMount(ReadyToMerge, { wrapper = shallowMount(ReadyToMerge, {
localVue,
propsData: { propsData: {
mr: createTestMr(customConfig), mr: createTestMr(customConfig),
service: createTestService(), service: createTestService(),
...@@ -82,10 +101,29 @@ const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) = ...@@ -82,10 +101,29 @@ const createComponent = (customConfig = {}, mergeRequestWidgetGraphql = false) =
stubs: { stubs: {
CommitEdit, CommitEdit,
}, },
apolloProvider: createMockApollo([[readyToMergeQuery, readyToMergeResponseSpy]]),
}); });
}; };
const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
const findCommitEditElements = () => wrapper.findAll(CommitEdit);
const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
const findFirstCommitEditLabel = () => findCommitEditElements().at(0).props('label');
const findTipLink = () => wrapper.find(GlSprintf);
const findCommitEditWithInputId = (inputId) =>
findCommitEditElements().wrappers.find((x) => x.props('inputId') === inputId);
const findMergeCommitMessage = () => findCommitEditWithInputId('merge-message-edit').props('value');
const findSquashCommitMessage = () =>
findCommitEditWithInputId('squash-message-edit').props('value');
const triggerApprovalUpdated = () => eventHub.$emit('ApprovalUpdated');
describe('ReadyToMerge', () => { describe('ReadyToMerge', () => {
beforeEach(() => {
readyToMergeResponseSpy = jest.fn().mockResolvedValueOnce(readyToMergeResponse);
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -447,13 +485,6 @@ describe('ReadyToMerge', () => { ...@@ -447,13 +485,6 @@ describe('ReadyToMerge', () => {
}); });
describe('render children components', () => { describe('render children components', () => {
const findCheckboxElement = () => wrapper.find(SquashBeforeMerge);
const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
const findCommitEditElements = () => wrapper.findAll(CommitEdit);
const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
const findFirstCommitEditLabel = () => findCommitEditElements().at(0).props('label');
const findTipLink = () => wrapper.find(GlSprintf);
describe('squash checkbox', () => { describe('squash checkbox', () => {
it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
createComponent({ createComponent({
...@@ -772,4 +803,65 @@ describe('ReadyToMerge', () => { ...@@ -772,4 +803,65 @@ describe('ReadyToMerge', () => {
expect(findPipelineFailedConfirmModal().props()).toEqual({ visible: true }); expect(findPipelineFailedConfirmModal().props()).toEqual({ visible: true });
}); });
}); });
describe('updating graphql data triggers commit message update when default changed', () => {
const UPDATED_MERGE_COMMIT_MESSAGE = 'New merge message from BE';
const UPDATED_SQUASH_COMMIT_MESSAGE = 'New squash message from BE';
const USER_COMMIT_MESSAGE = 'Merge message provided manually by user';
const createDefaultGqlComponent = () =>
createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true } }, true);
beforeEach(() => {
readyToMergeResponseSpy = jest
.fn()
.mockResolvedValueOnce(createReadyToMergeResponse({ squash: true, squashOnMerge: true }))
.mockResolvedValue(
createReadyToMergeResponse({
squash: true,
squashOnMerge: true,
defaultMergeCommitMessage: UPDATED_MERGE_COMMIT_MESSAGE,
defaultSquashCommitMessage: UPDATED_SQUASH_COMMIT_MESSAGE,
}),
);
});
describe.each`
desc | finderFn | initialValue | updatedValue | inputId
${'merge commit message'} | ${findMergeCommitMessage} | ${commitMessage} | ${UPDATED_MERGE_COMMIT_MESSAGE} | ${'#merge-message-edit'}
${'squash commit message'} | ${findSquashCommitMessage} | ${squashCommitMessage} | ${UPDATED_SQUASH_COMMIT_MESSAGE} | ${'#squash-message-edit'}
`('with $desc', ({ finderFn, initialValue, updatedValue, inputId }) => {
it('should have initial value', async () => {
createDefaultGqlComponent();
await waitForPromises();
expect(finderFn()).toBe(initialValue);
});
it('should have updated value after graphql refetch', async () => {
createDefaultGqlComponent();
await waitForPromises();
triggerApprovalUpdated();
await waitForPromises();
expect(finderFn()).toBe(updatedValue);
});
it('should not update if user has touched', async () => {
createDefaultGqlComponent();
await waitForPromises();
const input = wrapper.find(inputId);
input.element.value = USER_COMMIT_MESSAGE;
input.trigger('input');
triggerApprovalUpdated();
await waitForPromises();
expect(finderFn()).toBe(USER_COMMIT_MESSAGE);
});
});
});
}); });
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