Commit 16dcca08 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'tor/feature/unify-merging-states' into 'master'

Unify "merging" MR Widgets

See merge request gitlab-org/gitlab!68519
parents 3c5a3c7e 6b778dbf
......@@ -29,6 +29,7 @@ import {
WARNING,
MT_MERGE_STRATEGY,
PIPELINE_FAILED_STATE,
STATE_MACHINE,
} from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
......@@ -47,6 +48,9 @@ const MERGE_FAILED_STATUS = 'failed';
const MERGE_SUCCESS_STATUS = 'success';
const MERGE_HOOK_VALIDATION_ERROR_STATUS = 'hook_validation_error';
const { transitions } = STATE_MACHINE;
const { MERGE, MERGED, MERGE_FAILURE } = transitions;
export default {
name: 'ReadyToMerge',
apollo: {
......@@ -361,6 +365,7 @@ export default {
}
this.isMakingRequest = true;
this.mr.transitionStateMachine({ transition: MERGE });
this.service
.merge(options)
.then((res) => res.data)
......@@ -375,6 +380,7 @@ export default {
this.initiateMergePolling();
} else if (hasError) {
eventHub.$emit('FailedToMerge', data.merge_error);
this.mr.transitionStateMachine({ transition: MERGE_FAILURE });
}
if (this.glFeatures.mergeRequestWidgetGraphql) {
......@@ -383,6 +389,7 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
this.mr.transitionStateMachine({ transition: MERGE_FAILURE });
createFlash({
message: __('Something went wrong. Please try again.'),
});
......@@ -417,6 +424,7 @@ export default {
eventHub.$emit('FetchActionsContent');
MergeRequest.hideCloseButton();
MergeRequest.decreaseCounter();
this.mr.transitionStateMachine({ transition: MERGED });
stopPolling();
refreshUserMergeRequestCounts();
......@@ -428,6 +436,7 @@ export default {
}
} else if (data.merge_error) {
eventHub.$emit('FailedToMerge', data.merge_error);
this.mr.transitionStateMachine({ transition: MERGE_FAILURE });
stopPolling();
} else {
// MR is not merged yet, continue polling until the state becomes 'merged'
......@@ -438,6 +447,7 @@ export default {
createFlash({
message: __('Something went wrong while merging this merge request. Please try again.'),
});
this.mr.transitionStateMachine({ transition: MERGE_FAILURE });
stopPolling();
});
},
......
import { s__ } from '~/locale';
import { stateToComponentMap as classStateMap, stateKey } from './stores/state_maps';
export const SUCCESS = 'success';
export const WARNING = 'warning';
......@@ -52,3 +53,42 @@ export const MERGE_ACTIVE_STATUS_PHRASES = [
emoji: 'heart_eyes',
},
];
const STATE_MACHINE = {
states: {
IDLE: 'IDLE',
MERGING: 'MERGING',
},
transitions: {
MERGE: 'start-merge',
MERGE_FAILURE: 'merge-failed',
MERGED: 'merge-done',
},
};
const { states, transitions } = STATE_MACHINE;
STATE_MACHINE.definition = {
initial: states.IDLE,
states: {
[states.IDLE]: {
on: {
[transitions.MERGE]: states.MERGING,
},
},
[states.MERGING]: {
on: {
[transitions.MERGED]: states.IDLE,
[transitions.MERGE_FAILURE]: states.IDLE,
},
},
},
};
export const stateToTransitionMap = {
[stateKey.merging]: transitions.MERGE,
[stateKey.merged]: transitions.MERGED,
};
export const stateToComponentMap = {
[states.MERGING]: classStateMap[stateKey.merging],
};
export { STATE_MACHINE };
......@@ -4,7 +4,7 @@ import { isEmpty } from 'lodash';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import { stateToComponentMap as classState } from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
import createFlash from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import notify from '~/lib/utils/notify';
......@@ -39,6 +39,7 @@ import ShaMismatch from './components/states/sha_mismatch.vue';
import UnresolvedDiscussionsState from './components/states/unresolved_discussions.vue';
import WorkInProgressState from './components/states/work_in_progress.vue';
import ExtensionsContainer from './components/extensions/container';
import { STATE_MACHINE, stateToComponentMap } from './constants';
import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
......@@ -124,7 +125,9 @@ export default {
mr: store,
state: store && store.state,
service: store && this.createService(store),
machineState: store?.machineValue || STATE_MACHINE.definition.initial,
loading: true,
recomputeComponentName: 0,
};
},
computed: {
......@@ -139,7 +142,7 @@ export default {
return this.mr.state !== 'nothingToMerge';
},
componentName() {
return stateMaps.stateToComponentMap[this.mr.state];
return stateToComponentMap[this.machineState] || classState[this.mr.state];
},
hasPipelineMustSucceedConflict() {
return !this.mr.hasCI && this.mr.onlyAllowMergeIfPipelineSucceeds;
......@@ -206,6 +209,11 @@ export default {
},
},
watch: {
'mr.machineValue': {
handler(newValue) {
this.machineState = newValue;
},
},
state(newVal, oldVal) {
if (newVal !== oldVal && this.shouldRenderMergedPipeline) {
// init polling
......@@ -247,6 +255,8 @@ export default {
this.mr = new MRWidgetStore({ ...window.gl.mrWidgetData, ...data });
}
this.machineState = this.mr.machineValue;
if (!this.state) {
this.state = this.mr.state;
}
......
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { statusBoxState } from '~/issuable/components/status_box.vue';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
import { machine } from '~/lib/utils/finite_state_machine';
import {
MTWPS_MERGE_STRATEGY,
MT_MERGE_STRATEGY,
MWPS_MERGE_STRATEGY,
STATE_MACHINE,
stateToTransitionMap,
} from '../constants';
import { stateKey } from './state_maps';
const { format } = getTimeago();
const { states } = STATE_MACHINE;
const { IDLE } = states;
export default class MergeRequestStore {
constructor(data) {
this.sha = data.diff_head_sha;
......@@ -16,6 +26,9 @@ export default class MergeRequestStore {
this.apiUnapprovePath = data.api_unapprove_path;
this.hasApprovalsAvailable = data.has_approvals_available;
this.stateMachine = machine(STATE_MACHINE.definition);
this.machineValue = this.stateMachine.value;
this.setPaths(data);
this.setData(data);
......@@ -215,10 +228,7 @@ export default class MergeRequestStore {
setState() {
if (this.mergeOngoing) {
this.state = 'merging';
return;
}
if (this.isOpen) {
} else if (this.isOpen) {
this.state = getStateKey.call(this);
} else {
switch (this.mergeRequestState) {
......@@ -232,6 +242,8 @@ export default class MergeRequestStore {
this.state = null;
}
}
this.translateStateToMachine();
}
setPaths(data) {
......@@ -356,4 +368,32 @@ export default class MergeRequestStore {
(this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed)
);
}
// Because the state machine doesn't yet handle every state and transition,
// some use-cases will need to force a state that can't be reached by
// a known transition. This is undesirable long-term (as it subverts
// the intent of a state machine), but is necessary until the machine
// can handle all possible combinations. (unsafeForce)
transitionStateMachine({ transition, state, unsafeForce = false } = {}) {
if (unsafeForce && state) {
this.stateMachine.value = state;
} else {
this.stateMachine.send(transition);
}
this.machineValue = this.stateMachine.value;
}
translateStateToMachine() {
const transition = stateToTransitionMap[this.state];
let transitionOptions = {
state: IDLE,
unsafeForce: true,
};
if (transition) {
transitionOptions = { transition };
}
this.transitionStateMachine(transitionOptions);
}
}
const stateToComponentMap = {
export const stateToComponentMap = {
merged: 'mr-widget-merged',
closed: 'mr-widget-closed',
merging: 'mr-widget-merging',
......@@ -21,7 +21,7 @@ const stateToComponentMap = {
mergeChecksFailed: 'mergeChecksFailed',
};
const statesToShowHelpWidget = [
export const statesToShowHelpWidget = [
'merging',
'conflicts',
'workInProgress',
......@@ -50,11 +50,7 @@ export const stateKey = {
notAllowedToMerge: 'notAllowedToMerge',
readyToMerge: 'readyToMerge',
rebase: 'rebase',
merging: 'merging',
merged: 'merged',
mergeChecksFailed: 'mergeChecksFailed',
};
export default {
stateToComponentMap,
statesToShowHelpWidget,
};
import stateMaps from '~/vue_merge_request_widget/stores/state_maps';
stateMaps.stateToComponentMap.geoSecondaryNode = 'mr-widget-geo-secondary-node';
stateMaps.stateToComponentMap.policyViolation = 'mr-widget-policy-violation';
stateMaps.stateToComponentMap.jiraAssociationMissing = 'mr-widget-jira-association-missing';
import { stateToComponentMap as ceStateMap } from '~/vue_merge_request_widget/stores/state_maps';
export { statesToShowHelpWidget } from '~/vue_merge_request_widget/stores/state_maps';
export const stateKey = {
policyViolation: 'policyViolation',
jiraAssociationMissing: 'jiraAssociationMissing',
};
export default {
stateToComponentMap: stateMaps.stateToComponentMap,
statesToShowHelpWidget: stateMaps.statesToShowHelpWidget,
export const stateToComponentMap = {
...ceStateMap,
geoSecondaryNode: 'mr-widget-geo-secondary-node',
policyViolation: 'mr-widget-policy-violation',
jiraAssociationMissing: 'mr-widget-jira-association-missing',
};
......@@ -52,7 +52,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
find(':focus').send_keys :enter
expect(merge_button).to have_no_content('Merge in progress')
expect(merge_button).to have_content('Start merge train')
end
end
......@@ -62,7 +62,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
click_button 'Merge immediately'
expect(merge_button).to have_content('Merge in progress')
expect(find('.media-body h4')).to have_content('Merging!')
end
end
end
......
......@@ -36,7 +36,7 @@ RSpec.describe 'Merge requests > User merges immediately', :js do
Sidekiq::Testing.fake! do
click_button 'Merge immediately'
expect(find('.accept-merge-request.btn-confirm')).to have_content('Merge in progress')
expect(find('.media-body h4')).to have_content('Merging!')
wait_for_requests
end
......
......@@ -45,6 +45,8 @@ const createTestMr = (customConfig) => {
preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY,
availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY],
mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs',
transitionStateMachine: () => eventHub.$emit('StateMachineValueChanged', { value: 'value' }),
translateStateToMachine: () => this.transitionStateMachine(),
};
Object.assign(mr, customConfig.mr);
......
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