Commit 6112adcc authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch 'nmezzopera-merge-conflict-store-connect' into 'master'

Swap custom store for vuex implementation in merge_conflicts

See merge request gitlab-org/gitlab!56756
parents 96e28bc8 e9d48450
<script>
import { debounce } from 'lodash';
import { mapActions } from 'vuex';
import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { INTERACTIVE_RESOLVE_MODE } from '../constants';
export default {
props: {
......@@ -10,14 +12,6 @@ export default {
type: Object,
required: true,
},
onCancelDiscardConfirmation: {
type: Function,
required: true,
},
onAcceptDiscardConfirmation: {
type: Function,
required: true,
},
},
data() {
return {
......@@ -50,6 +44,7 @@ export default {
}
},
methods: {
...mapActions(['setFileResolveMode', 'setPromptConfirmationState', 'updateFile']),
loadEditor() {
const EditorPromise = import(/* webpackChunkName: 'EditorLite' */ '~/editor/editor_lite');
const DataPromise = axios.get(this.file.content_path);
......@@ -82,23 +77,24 @@ export default {
saveDiffResolution() {
this.saved = true;
// This probably be better placed in the data provider
/* eslint-disable vue/no-mutating-props */
this.file.content = this.editor.getValue();
this.file.resolveEditChanged = this.file.content !== this.originalContent;
this.file.promptDiscardConfirmation = false;
/* eslint-enable vue/no-mutating-props */
this.updateFile({
...this.file,
content: this.editor.getValue(),
resolveEditChanged: this.file.content !== this.originalContent,
promptDiscardConfirmation: false,
});
},
resetEditorContent() {
if (this.fileLoaded) {
this.editor.setValue(this.originalContent);
}
},
cancelDiscardConfirmation(file) {
this.onCancelDiscardConfirmation(file);
},
acceptDiscardConfirmation(file) {
this.onAcceptDiscardConfirmation(file);
this.setPromptConfirmationState({ file, promptDiscardConfirmation: false });
this.setFileResolveMode({ file, mode: INTERACTIVE_RESOLVE_MODE });
},
cancelDiscardConfirmation(file) {
this.setPromptConfirmationState({ file, promptDiscardConfirmation: false });
},
},
};
......
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import actionsMixin from '../mixins/line_conflict_actions';
import { mapActions } from 'vuex';
import syntaxHighlight from '~/syntax_highlight';
import { SYNTAX_HIGHLIGHT_CLASS } from '../constants';
import utilsMixin from '../mixins/line_conflict_utils';
export default {
directives: {
SafeHtml,
},
mixins: [utilsMixin, actionsMixin],
mixins: [utilsMixin],
SYNTAX_HIGHLIGHT_CLASS,
props: {
file: {
type: Object,
required: true,
},
},
mounted() {
syntaxHighlight(document.querySelectorAll(`.${SYNTAX_HIGHLIGHT_CLASS}`));
},
methods: {
...mapActions(['handleSelected']),
},
};
</script>
<template>
<table class="diff-wrap-lines code code-commit js-syntax-highlight">
<tr
v-for="line in file.inlineLines"
:key="(line.isHeader ? line.id : line.new_line) + line.richText"
class="line_holder diff-inline"
>
<table :class="['diff-wrap-lines code code-commit', $options.SYNTAX_HIGHLIGHT_CLASS]">
<!-- Unfortunately there isn't a good key for these sections -->
<!-- eslint-disable vue/require-v-for-key -->
<tr v-for="line in file.inlineLines" class="line_holder diff-inline">
<template v-if="line.isHeader">
<td :class="lineCssClass(line)" class="diff-line-num header"></td>
<td :class="lineCssClass(line)" class="diff-line-num header"></td>
<td :class="lineCssClass(line)" class="line_content header">
<strong>{{ line.richText }}</strong>
<button class="btn" @click="handleSelected(file, line.id, line.section)">
<button class="btn" @click="handleSelected({ file, line })">
{{ line.buttonTitle }}
</button>
</td>
......
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import actionsMixin from '../mixins/line_conflict_actions';
import { mapActions } from 'vuex';
import syntaxHighlight from '~/syntax_highlight';
import { SYNTAX_HIGHLIGHT_CLASS } from '../constants';
import utilsMixin from '../mixins/line_conflict_utils';
export default {
directives: {
SafeHtml,
},
mixins: [utilsMixin, actionsMixin],
mixins: [utilsMixin],
SYNTAX_HIGHLIGHT_CLASS,
props: {
file: {
type: Object,
required: true,
},
},
mounted() {
syntaxHighlight(document.querySelectorAll(`.${SYNTAX_HIGHLIGHT_CLASS}`));
},
methods: {
...mapActions(['handleSelected']),
},
};
</script>
<template>
<!-- Unfortunately there isn't a good key for these sections -->
<!-- eslint-disable vue/require-v-for-key -->
<table class="diff-wrap-lines code js-syntax-highlight">
<table :class="['diff-wrap-lines code', $options.SYNTAX_HIGHLIGHT_CLASS]">
<tr v-for="section in file.parallelLines" class="line_holder parallel">
<template v-for="line in section">
<template v-if="line.isHeader">
<td class="diff-line-num header" :class="lineCssClass(line)"></td>
<td class="line_content header" :class="lineCssClass(line)">
<strong>{{ line.richText }}</strong>
<button class="btn" @click="handleSelected(file, line.id, line.section)">
<button class="btn" @click="handleSelected({ file, line })">
{{ line.buttonTitle }}
</button>
</td>
......
......@@ -13,6 +13,7 @@ export const VIEW_TYPES = {
export const EDIT_RESOLVE_MODE = 'edit';
export const INTERACTIVE_RESOLVE_MODE = 'interactive';
export const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE;
export const SYNTAX_HIGHLIGHT_CLASS = 'js-syntax-highlight';
export const HEAD_HEADER_TEXT = s__('MergeConflict|HEAD//our changes');
export const ORIGIN_HEADER_TEXT = s__('MergeConflict|origin//their changes');
......
<script>
import { GlSprintf } from '@gitlab/ui';
import { mapGetters, mapState, mapActions } from 'vuex';
import { __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import DiffFileEditor from './components/diff_file_editor.vue';
import InlineConflictLines from './components/inline_conflict_lines.vue';
import ParallelConflictLines from './components/parallel_conflict_lines.vue';
import { INTERACTIVE_RESOLVE_MODE } from './constants';
/**
* NOTE: Most of this component is directly using $root, rather than props or a better data store.
* This is BAD and one shouldn't copy that behavior. Similarly a lot of the classes below should
* A lot of the classes below should
* be replaced with GitLab UI components.
*
* We are just doing it temporarily in order to migrate the template from HAML => Vue in an iterative manner
......@@ -25,60 +26,88 @@ export default {
InlineConflictLines,
ParallelConflictLines,
},
inject: ['mergeRequestPath', 'sourceBranchPath'],
inject: ['mergeRequestPath', 'sourceBranchPath', 'resolveConflictsPath'],
i18n: {
commitStatSummary: __('Showing %{conflict} between %{sourceBranch} and %{targetBranch}'),
resolveInfo: __(
'You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}',
),
},
computed: {
...mapGetters([
'getConflictsCountText',
'isReadyToCommit',
'getCommitButtonText',
'fileTextTypePresent',
]),
...mapState(['isLoading', 'hasError', 'isParallel', 'conflictsData']),
commitMessage: {
get() {
return this.conflictsData.commitMessage;
},
set(value) {
this.updateCommitMessage(value);
},
},
},
methods: {
...mapActions([
'setViewType',
'submitResolvedConflicts',
'setFileResolveMode',
'setPromptConfirmationState',
'updateCommitMessage',
]),
onClickResolveModeButton(file, mode) {
if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) {
this.setPromptConfirmationState({ file, promptDiscardConfirmation: true });
} else {
this.setFileResolveMode({ file, mode });
}
},
},
};
</script>
<template>
<div id="conflicts">
<div v-if="$root.isLoading" class="loading">
<div v-if="isLoading" class="loading">
<div class="spinner spinner-md"></div>
</div>
<div v-if="$root.hasError" class="nothing-here-block">
{{ $root.conflictsData.errorMessage }}
<div v-if="hasError" class="nothing-here-block">
{{ conflictsData.errorMessage }}
</div>
<template v-if="!$root.isLoading && !$root.hasError">
<template v-if="!isLoading && !hasError">
<div class="content-block oneline-block files-changed">
<div v-if="$root.showDiffViewTypeSwitcher" class="inline-parallel-buttons">
<div v-if="fileTextTypePresent" class="inline-parallel-buttons">
<div class="btn-group">
<button
:class="{ active: !$root.isParallel }"
:class="{ active: !isParallel }"
class="btn gl-button"
@click="$root.handleViewTypeChange('inline')"
@click="setViewType('inline')"
>
{{ __('Inline') }}
</button>
<button
:class="{ active: $root.isParallel }"
:class="{ active: isParallel }"
class="btn gl-button"
@click="$root.handleViewTypeChange('parallel')"
data-testid="side-by-side"
@click="setViewType('parallel')"
>
{{ __('Side-by-side') }}
</button>
</div>
</div>
<div class="js-toggle-container">
<div class="commit-stat-summary">
<div class="commit-stat-summary" data-testid="conflicts-count">
<gl-sprintf :message="$options.i18n.commitStatSummary">
<template #conflict>
<strong class="cred">
{{ $root.conflictsCountText }}
</strong>
<strong class="cred">{{ getConflictsCountText }}</strong>
</template>
<template #sourceBranch>
<strong class="ref-name">
{{ $root.conflictsData.sourceBranch }}
</strong>
<strong class="ref-name">{{ conflictsData.sourceBranch }}</strong>
</template>
<template #targetBranch>
<strong class="ref-name">
{{ $root.conflictsData.targetBranch }}
</strong>
<strong class="ref-name">{{ conflictsData.targetBranch }}</strong>
</template>
</gl-sprintf>
</div>
......@@ -87,12 +116,13 @@ export default {
<div class="files-wrapper">
<div class="files">
<div
v-for="file in $root.conflictsData.files"
v-for="file in conflictsData.files"
:key="file.blobPath"
class="diff-file file-holder conflict"
data-testid="files"
>
<div class="js-file-title file-title file-title-flex-parent cursor-default">
<div class="file-header-content">
<div class="file-header-content" data-testid="file-name">
<file-icon :file-name="file.filePath" :size="18" css-classes="gl-mr-2" />
<strong class="file-title-name">{{ file.filePath }}</strong>
</div>
......@@ -102,7 +132,8 @@ export default {
:class="{ active: file.resolveMode === 'interactive' }"
class="btn gl-button"
type="button"
@click="$root.onClickResolveModeButton(file, 'interactive')"
data-testid="interactive-button"
@click="onClickResolveModeButton(file, 'interactive')"
>
{{ __('Interactive mode') }}
</button>
......@@ -110,7 +141,8 @@ export default {
:class="{ active: file.resolveMode === 'edit' }"
class="btn gl-button"
type="button"
@click="$root.onClickResolveModeButton(file, 'edit')"
data-testid="inline-button"
@click="onClickResolveModeButton(file, 'edit')"
>
{{ __('Edit inline') }}
</button>
......@@ -118,35 +150,23 @@ export default {
<a :href="file.blobPath" class="btn gl-button view-file">
<gl-sprintf :message="__('View file @ %{commitSha}')">
<template #commitSha>
{{ $root.conflictsData.shortCommitSha }}
{{ conflictsData.shortCommitSha }}
</template>
</gl-sprintf>
</a>
</div>
</div>
<div class="diff-content diff-wrap-lines">
<div
v-show="
!$root.isParallel && file.resolveMode === 'interactive' && file.type === 'text'
"
class="file-content"
>
<inline-conflict-lines :file="file" />
</div>
<div
v-show="
$root.isParallel && file.resolveMode === 'interactive' && file.type === 'text'
"
class="file-content"
>
<parallel-conflict-lines :file="file" />
</div>
<div v-show="file.resolveMode === 'edit' || file.type === 'text-editor'">
<diff-file-editor
:file="file"
:on-accept-discard-confirmation="$root.acceptDiscardConfirmation"
:on-cancel-discard-confirmation="$root.cancelDiscardConfirmation"
/>
<template v-if="file.resolveMode === 'interactive' && file.type === 'text'">
<div v-if="!isParallel" class="file-content">
<inline-conflict-lines :file="file" />
</div>
<div v-if="isParallel" class="file-content">
<parallel-conflict-lines :file="file" />
</div>
</template>
<div v-if="file.resolveMode === 'edit' || file.type === 'text-editor'">
<diff-file-editor :file="file" />
</div>
</div>
</div>
......@@ -169,7 +189,7 @@ export default {
</template>
<template #branch_name>
<a class="ref-name" :href="sourceBranchPath">
{{ $root.conflictsData.sourceBranch }}
{{ conflictsData.sourceBranch }}
</a>
</template>
</gl-sprintf>
......@@ -183,7 +203,8 @@ export default {
<div class="max-width-marker"></div>
<textarea
id="commit-message"
v-model="$root.conflictsData.commitMessage"
v-model="commitMessage"
data-testid="commit-message"
class="form-control js-commit-message"
rows="5"
></textarea>
......@@ -195,12 +216,12 @@ export default {
<div class="row">
<div class="col-6">
<button
:disabled="!$root.readyToCommit"
:disabled="!isReadyToCommit"
class="btn gl-button btn-success js-submit-button"
type="button"
@click="$root.commit()"
@click="submitResolvedConflicts(resolveConflictsPath)"
>
<span>{{ $root.commitButtonText }}</span>
<span>{{ getCommitButtonText }}</span>
</button>
</div>
<div class="col-6 text-right">
......
import axios from '../lib/utils/axios_utils';
export default class MergeConflictsService {
constructor(options) {
this.conflictsPath = options.conflictsPath;
this.resolveConflictsPath = options.resolveConflictsPath;
}
fetchConflictsData() {
return axios.get(this.conflictsPath);
}
submitResolveConflicts(data) {
return axios.post(this.resolveConflictsPath, data);
}
}
/* eslint-disable no-param-reassign, babel/camelcase, no-nested-ternary, no-continue */
import $ from 'jquery';
import Cookies from 'js-cookie';
import Vue from 'vue';
import { s__ } from '~/locale';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
const diffViewType = Cookies.get('diff_view');
const HEAD_HEADER_TEXT = s__('MergeConflict|HEAD//our changes');
const ORIGIN_HEADER_TEXT = s__('MergeConflict|origin//their changes');
const HEAD_BUTTON_TITLE = s__('MergeConflict|Use ours');
const ORIGIN_BUTTON_TITLE = s__('MergeConflict|Use theirs');
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const EDIT_RESOLVE_MODE = 'edit';
const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE;
const VIEW_TYPES = {
INLINE: 'inline',
PARALLEL: 'parallel',
};
const CONFLICT_TYPES = {
TEXT: 'text',
TEXT_EDITOR: 'text-editor',
};
global.mergeConflicts.mergeConflictsStore = {
state: {
isLoading: true,
hasError: false,
isSubmitting: false,
isParallel: diffViewType === VIEW_TYPES.PARALLEL,
diffViewType,
conflictsData: {},
},
setConflictsData(data) {
this.decorateFiles(data.files);
this.state.conflictsData = {
files: data.files,
commitMessage: data.commit_message,
sourceBranch: data.source_branch,
targetBranch: data.target_branch,
shortCommitSha: data.commit_sha.slice(0, 7),
};
},
decorateFiles(files) {
files.forEach((file) => {
file.content = '';
file.resolutionData = {};
file.promptDiscardConfirmation = false;
file.resolveMode = DEFAULT_RESOLVE_MODE;
file.filePath = this.getFilePath(file);
file.blobPath = file.blob_path;
if (file.type === CONFLICT_TYPES.TEXT) {
file.showEditor = false;
file.loadEditor = false;
this.setInlineLine(file);
this.setParallelLine(file);
} else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
file.showEditor = true;
file.loadEditor = true;
}
});
},
setInlineLine(file) {
file.inlineLines = [];
file.sections.forEach((section) => {
let currentLineType = 'new';
const { conflict, lines, id } = section;
if (conflict) {
file.inlineLines.push(this.getHeadHeaderLine(id));
}
lines.forEach((line) => {
const { type } = line;
if ((type === 'new' || type === 'old') && currentLineType !== type) {
currentLineType = type;
file.inlineLines.push({ lineType: 'emptyLine', richText: '' });
}
this.decorateLineForInlineView(line, id, conflict);
file.inlineLines.push(line);
});
if (conflict) {
file.inlineLines.push(this.getOriginHeaderLine(id));
}
});
},
setParallelLine(file) {
file.parallelLines = [];
const linesObj = { left: [], right: [] };
file.sections.forEach((section) => {
const { conflict, lines, id } = section;
if (conflict) {
linesObj.left.push(this.getOriginHeaderLine(id));
linesObj.right.push(this.getHeadHeaderLine(id));
}
lines.forEach((line) => {
const { type } = line;
if (conflict) {
if (type === 'old') {
linesObj.left.push(this.getLineForParallelView(line, id, 'conflict'));
} else if (type === 'new') {
linesObj.right.push(this.getLineForParallelView(line, id, 'conflict', true));
}
} else {
const lineType = type || 'context';
linesObj.left.push(this.getLineForParallelView(line, id, lineType));
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
}
});
this.checkLineLengths(linesObj);
});
for (let i = 0, len = linesObj.left.length; i < len; i += 1) {
file.parallelLines.push([linesObj.right[i], linesObj.left[i]]);
}
},
setLoadingState(state) {
this.state.isLoading = state;
},
setErrorState(state) {
this.state.hasError = state;
},
setFailedRequest(message) {
this.state.hasError = true;
this.state.conflictsData.errorMessage = message;
},
getConflictsCount() {
if (!this.state.conflictsData.files.length) {
return 0;
}
const { files } = this.state.conflictsData;
let count = 0;
files.forEach((file) => {
if (file.type === CONFLICT_TYPES.TEXT) {
file.sections.forEach((section) => {
if (section.conflict) {
count += 1;
}
});
} else {
count += 1;
}
});
return count;
},
getConflictsCountText() {
const count = this.getConflictsCount();
const text = count > 1 ? s__('MergeConflict|conflicts') : s__('MergeConflict|conflict');
return `${count} ${text}`;
},
setViewType(viewType) {
this.state.diffView = viewType;
this.state.isParallel = viewType === VIEW_TYPES.PARALLEL;
Cookies.set('diff_view', viewType);
},
getHeadHeaderLine(id) {
return {
id,
richText: HEAD_HEADER_TEXT,
buttonTitle: HEAD_BUTTON_TITLE,
type: 'new',
section: 'head',
isHeader: true,
isHead: true,
isSelected: false,
isUnselected: false,
};
},
decorateLineForInlineView(line, id, conflict) {
const { type } = line;
line.id = id;
line.hasConflict = conflict;
line.isHead = type === 'new';
line.isOrigin = type === 'old';
line.hasMatch = type === 'match';
line.richText = line.rich_text;
line.isSelected = false;
line.isUnselected = false;
},
getLineForParallelView(line, id, lineType, isHead) {
const { old_line, new_line, rich_text } = line;
const hasConflict = lineType === 'conflict';
return {
id,
lineType,
hasConflict,
isHead: hasConflict && isHead,
isOrigin: hasConflict && !isHead,
hasMatch: lineType === 'match',
lineNumber: isHead ? new_line : old_line,
section: isHead ? 'head' : 'origin',
richText: rich_text,
isSelected: false,
isUnselected: false,
};
},
getOriginHeaderLine(id) {
return {
id,
richText: ORIGIN_HEADER_TEXT,
buttonTitle: ORIGIN_BUTTON_TITLE,
type: 'old',
section: 'origin',
isHeader: true,
isOrigin: true,
isSelected: false,
isUnselected: false,
};
},
getFilePath(file) {
const { old_path, new_path } = file;
return old_path === new_path ? new_path : `${old_path}${new_path}`;
},
checkLineLengths(linesObj) {
const { left, right } = linesObj;
if (left.length !== right.length) {
if (left.length > right.length) {
const diff = left.length - right.length;
for (let i = 0; i < diff; i += 1) {
right.push({ lineType: 'emptyLine', richText: '' });
}
} else {
const diff = right.length - left.length;
for (let i = 0; i < diff; i += 1) {
left.push({ lineType: 'emptyLine', richText: '' });
}
}
}
},
setPromptConfirmationState(file, state) {
file.promptDiscardConfirmation = state;
},
setFileResolveMode(file, mode) {
if (mode === INTERACTIVE_RESOLVE_MODE) {
file.showEditor = false;
} else if (mode === EDIT_RESOLVE_MODE) {
// Restore Interactive mode when switching to Edit mode
file.showEditor = true;
file.loadEditor = true;
file.resolutionData = {};
this.restoreFileLinesState(file);
}
file.resolveMode = mode;
},
restoreFileLinesState(file) {
file.inlineLines.forEach((line) => {
if (line.hasConflict || line.isHeader) {
line.isSelected = false;
line.isUnselected = false;
}
});
file.parallelLines.forEach((lines) => {
const left = lines[0];
const right = lines[1];
const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader;
if (isLeftMatch || isRightMatch) {
left.isSelected = false;
left.isUnselected = false;
right.isSelected = false;
right.isUnselected = false;
}
});
},
isReadyToCommit() {
const { files } = this.state.conflictsData;
const hasCommitMessage = $.trim(this.state.conflictsData.commitMessage).length;
let unresolved = 0;
for (let i = 0, l = files.length; i < l; i += 1) {
const file = files[i];
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
let numberConflicts = 0;
const resolvedConflicts = Object.keys(file.resolutionData).length;
// We only check for conflicts type 'text'
// since conflicts `text_editor` can´t be resolved in interactive mode
if (file.type === CONFLICT_TYPES.TEXT) {
for (let j = 0, k = file.sections.length; j < k; j += 1) {
if (file.sections[j].conflict) {
numberConflicts += 1;
}
}
if (resolvedConflicts !== numberConflicts) {
unresolved += 1;
}
}
} else if (file.resolveMode === EDIT_RESOLVE_MODE) {
// Unlikely to happen since switching to Edit mode saves content automatically.
// Checking anyway in case the save strategy changes in the future
if (!file.content) {
unresolved += 1;
continue;
}
}
}
return !this.state.isSubmitting && hasCommitMessage && !unresolved;
},
getCommitButtonText() {
const initial = s__('MergeConflict|Commit to source branch');
const inProgress = s__('MergeConflict|Committing...');
return this.state ? (this.state.isSubmitting ? inProgress : initial) : initial;
},
getCommitData() {
let commitData = {};
commitData = {
commit_message: this.state.conflictsData.commitMessage,
files: [],
};
this.state.conflictsData.files.forEach((file) => {
const addFile = {
old_path: file.old_path,
new_path: file.new_path,
};
if (file.type === CONFLICT_TYPES.TEXT) {
// Submit only one data for type of editing
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
addFile.sections = file.resolutionData;
} else if (file.resolveMode === EDIT_RESOLVE_MODE) {
addFile.content = file.content;
}
} else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
addFile.content = file.content;
}
commitData.files.push(addFile);
});
return commitData;
},
handleSelected(file, sectionId, selection) {
Vue.set(file.resolutionData, sectionId, selection);
file.inlineLines.forEach((line) => {
if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
this.markLine(line, selection);
}
});
file.parallelLines.forEach((lines) => {
const left = lines[0];
const right = lines[1];
const hasSameId = right.id === sectionId || left.id === sectionId;
const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader;
if (hasSameId && (isLeftMatch || isRightMatch)) {
this.markLine(left, selection);
this.markLine(right, selection);
}
});
},
markLine(line, selection) {
if (selection === 'head' && line.isHead) {
line.isSelected = true;
line.isUnselected = false;
} else if (selection === 'origin' && line.isOrigin) {
line.isSelected = true;
line.isUnselected = false;
} else {
line.isSelected = false;
line.isUnselected = true;
}
},
setSubmitState(state) {
this.state.isSubmitting = state;
},
fileTextTypePresent() {
return this.state.conflictsData.files.some((f) => f.type === CONFLICT_TYPES.TEXT);
},
};
})(window.gl || (window.gl = {}));
import $ from 'jquery';
import Vue from 'vue';
import { __ } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '../flash';
import initIssuableSidebar from '../init_issuable_sidebar';
import './merge_conflict_store';
import syntaxHighlight from '../syntax_highlight';
import MergeConflictsResolverApp from './merge_conflict_resolver_app.vue';
import MergeConflictsService from './merge_conflict_service';
import { createStore } from './store';
export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts');
const { mergeConflictsStore } = gl.mergeConflicts;
const mergeConflictsService = new MergeConflictsService({
conflictsPath: conflictsEl.dataset.conflictsPath,
resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath,
});
const { sourceBranchPath, mergeRequestPath } = conflictsEl.dataset;
const {
sourceBranchPath,
mergeRequestPath,
conflictsPath,
resolveConflictsPath,
} = conflictsEl.dataset;
initIssuableSidebar();
const store = createStore();
return new Vue({
el: conflictsEl,
store,
provide: {
sourceBranchPath,
mergeRequestPath,
},
data: mergeConflictsStore.state,
computed: {
conflictsCountText() {
return mergeConflictsStore.getConflictsCountText();
},
readyToCommit() {
return mergeConflictsStore.isReadyToCommit();
},
commitButtonText() {
return mergeConflictsStore.getCommitButtonText();
},
showDiffViewTypeSwitcher() {
return mergeConflictsStore.fileTextTypePresent();
},
resolveConflictsPath,
},
created() {
mergeConflictsService
.fetchConflictsData()
.then(({ data }) => {
if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message);
} else {
mergeConflictsStore.setConflictsData(data);
}
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
syntaxHighlight($('.js-syntax-highlight'));
});
})
.catch(() => {
mergeConflictsStore.setLoadingState(false);
mergeConflictsStore.setFailedRequest();
});
},
methods: {
handleViewTypeChange(viewType) {
mergeConflictsStore.setViewType(viewType);
},
onClickResolveModeButton(file, mode) {
if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) {
mergeConflictsStore.setPromptConfirmationState(file, true);
return;
}
mergeConflictsStore.setFileResolveMode(file, mode);
},
acceptDiscardConfirmation(file) {
mergeConflictsStore.setPromptConfirmationState(file, false);
mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE);
},
cancelDiscardConfirmation(file) {
mergeConflictsStore.setPromptConfirmationState(file, false);
},
commit() {
mergeConflictsStore.setSubmitState(true);
mergeConflictsService
.submitResolveConflicts(mergeConflictsStore.getCommitData())
.then(({ data }) => {
window.location.href = data.redirect_to;
})
.catch(() => {
mergeConflictsStore.setSubmitState(false);
createFlash(__('Failed to save merge conflicts resolutions. Please try again!'));
});
},
store.dispatch('fetchConflictsData', conflictsPath);
},
render(createElement) {
return createElement(MergeConflictsResolverApp);
......
export default {
methods: {
handleSelected(file, sectionId, selection) {
gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection);
},
},
};
......@@ -118,3 +118,8 @@ export const handleSelected = ({ commit, state, getters }, { file, line: { id, s
commit(types.UPDATE_FILE, { file: updated, index });
};
export const updateFile = ({ commit, getters }, file) => {
const index = getters.getFileIndex(file);
commit(types.UPDATE_FILE, { file, index });
};
import { GlSprintf } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import InlineConflictLines from '~/merge_conflicts/components/inline_conflict_lines.vue';
import ParallelConflictLines from '~/merge_conflicts/components/parallel_conflict_lines.vue';
import component from '~/merge_conflicts/merge_conflict_resolver_app.vue';
import { createStore } from '~/merge_conflicts/store';
import { decorateFiles } from '~/merge_conflicts/utils';
import { conflictsMock } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Merge Conflict Resolver App', () => {
let wrapper;
let store;
const decoratedMockFiles = decorateFiles(conflictsMock.files);
const mountComponent = () => {
wrapper = shallowMount(component, {
store,
stubs: { GlSprintf },
provide() {
return {
mergeRequestPath: 'foo',
sourceBranchPath: 'foo',
resolveConflictsPath: 'bar',
};
},
});
};
beforeEach(() => {
store = createStore();
store.commit('SET_LOADING_STATE', false);
store.dispatch('setConflictsData', conflictsMock);
});
afterEach(() => {
wrapper.destroy();
});
const findConflictsCount = () => wrapper.find('[data-testid="conflicts-count"]');
const findFiles = () => wrapper.findAll('[data-testid="files"]');
const findFileHeader = (w = wrapper) => w.find('[data-testid="file-name"]');
const findFileInteractiveButton = (w = wrapper) => w.find('[data-testid="interactive-button"]');
const findFileInlineButton = (w = wrapper) => w.find('[data-testid="inline-button"]');
const findSideBySideButton = () => wrapper.find('[data-testid="side-by-side"]');
const findInlineConflictLines = (w = wrapper) => w.find(InlineConflictLines);
const findParallelConflictLines = (w = wrapper) => w.find(ParallelConflictLines);
const findCommitMessageTextarea = () => wrapper.find('[data-testid="commit-message"]');
it('shows the amount of conflicts', () => {
mountComponent();
const title = findConflictsCount();
expect(title.exists()).toBe(true);
expect(title.text().trim()).toBe('Showing 3 conflicts between test-conflicts and master');
});
describe('files', () => {
it('shows one file area for each file', () => {
mountComponent();
expect(findFiles()).toHaveLength(conflictsMock.files.length);
});
it('has the appropriate file header', () => {
mountComponent();
const fileHeader = findFileHeader(findFiles().at(0));
expect(fileHeader.text()).toBe(decoratedMockFiles[0].filePath);
});
describe('editing', () => {
it('interactive mode is the default', () => {
mountComponent();
const interactiveButton = findFileInteractiveButton(findFiles().at(0));
const inlineButton = findFileInlineButton(findFiles().at(0));
expect(interactiveButton.classes('active')).toBe(true);
expect(inlineButton.classes('active')).toBe(false);
});
it('clicking inline set inline as default', async () => {
mountComponent();
const inlineButton = findFileInlineButton(findFiles().at(0));
expect(inlineButton.classes('active')).toBe(false);
inlineButton.trigger('click');
await wrapper.vm.$nextTick();
expect(inlineButton.classes('active')).toBe(true);
});
it('inline mode shows a inline-conflict-lines', () => {
mountComponent();
const inlineConflictLinesComponent = findInlineConflictLines(findFiles().at(0));
expect(inlineConflictLinesComponent.exists()).toBe(true);
expect(inlineConflictLinesComponent.props('file')).toEqual(decoratedMockFiles[0]);
});
it('parallel mode shows a parallel-conflict-lines', async () => {
mountComponent();
findSideBySideButton().trigger('click');
await wrapper.vm.$nextTick();
const parallelConflictLinesComponent = findParallelConflictLines(findFiles().at(0));
expect(parallelConflictLinesComponent.exists()).toBe(true);
expect(parallelConflictLinesComponent.props('file')).toEqual(decoratedMockFiles[0]);
});
});
});
describe('submit form', () => {
it('contains a commit message textarea', () => {
mountComponent();
expect(findCommitMessageTextarea().exists()).toBe(true);
});
});
});
export const conflictsMock = {
target_branch: 'master',
source_branch: 'test-conflicts',
commit_sha: '6dbf385a3c7bf01e09b5d2d9e5d72f8fb8c590a3',
commit_message:
"Merge branch 'master' into 'test-conflicts'\n\n# Conflicts:\n# .gitlab-ci.yml\n# README.md",
files: [
{
old_path: '.gitlab-ci.yml',
new_path: '.gitlab-ci.yml',
blob_icon: 'doc-text',
blob_path:
'/gitlab-org/gitlab-test/-/blob/6dbf385a3c7bf01e09b5d2d9e5d72f8fb8c590a3/.gitlab-ci.yml',
sections: [
{
conflict: false,
lines: [
{
line_code: null,
type: 'match',
old_line: null,
new_line: null,
text: '@@ -7,10 +7,11 @@ upload:',
meta_data: { old_pos: 7, new_pos: 7 },
rich_text: '@@ -7,10 +7,11 @@ upload:',
can_receive_suggestion: true,
},
{
line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_7_7',
type: null,
old_line: 7,
new_line: 7,
text: ' stage: upload',
meta_data: null,
rich_text:
'\u003cspan id="LC7" class="line" lang="yaml"\u003e \u003cspan class="na"\u003estage\u003c/span\u003e\u003cspan class="pi"\u003e:\u003c/span\u003e \u003cspan class="s"\u003eupload\u003c/span\u003e\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_8_8',
type: null,
old_line: 8,
new_line: 8,
text: ' script:',
meta_data: null,
rich_text:
'\u003cspan id="LC8" class="line" lang="yaml"\u003e \u003cspan class="na"\u003escript\u003c/span\u003e\u003cspan class="pi"\u003e:\u003c/span\u003e\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_9_9',
type: null,
old_line: 9,
new_line: 9,
text:
// eslint-disable-next-line no-template-curly-in-string
' - \'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file README.md ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/my_package/0.0.1/file.txt\'',
meta_data: null,
rich_text:
// eslint-disable-next-line no-template-curly-in-string
'\u003cspan id="LC9" class="line" lang="yaml"\u003e \u003cspan class="pi"\u003e-\u003c/span\u003e \u003cspan class="s1"\u003e\'\u003c/span\u003e\u003cspan class="s"\u003ecurl\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e--header\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e"JOB-TOKEN:\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e$CI_JOB_TOKEN"\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e--upload-file\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003eREADME.md\u003c/span\u003e\u003cspan class="nv"\u003e \u003c/span\u003e\u003cspan class="s"\u003e${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/my_package/0.0.1/file.txt\'\u003c/span\u003e\u003c/span\u003e\n',
can_receive_suggestion: true,
},
],
},
{
conflict: true,
lines: [
{
line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_10_10',
type: 'new',
old_line: null,
new_line: 10,
text: '# some new comments',
meta_data: null,
rich_text:
'\u003cspan id="LC10" class="line" lang="yaml"\u003e\u003cspan class="c1"\u003e# some new comments\u003c/span\u003e\u003c/span\u003e',
can_receive_suggestion: true,
},
{
line_code: '587d266bb27a4dc3022bbed44dfa19849df3044c_10_11',
type: 'old',
old_line: 10,
new_line: null,
text: '# a different comment',
meta_data: null,
rich_text:
'\u003cspan id="LC10" class="line" lang="yaml"\u003e\u003cspan class="c1"\u003e# a different comment\u003c/span\u003e\u003c/span\u003e',
can_receive_suggestion: false,
},
],
id: '587d266bb27a4dc3022bbed44dfa19849df3044c_10_10',
},
],
type: 'text',
content_path:
'/gitlab-org/gitlab-test/-/merge_requests/2/conflict_for_path?new_path=.gitlab-ci.yml\u0026old_path=.gitlab-ci.yml',
},
{
old_path: 'README.md',
new_path: 'README.md',
blob_icon: 'doc-text',
blob_path:
'/gitlab-org/gitlab-test/-/blob/6dbf385a3c7bf01e09b5d2d9e5d72f8fb8c590a3/README.md',
sections: [
{
conflict: false,
lines: [
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_1_1',
type: null,
old_line: 1,
new_line: 1,
text: '- 1',
meta_data: null,
rich_text:
'\u003cspan id="LC1" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 1\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_2_2',
type: null,
old_line: 2,
new_line: 2,
text: '- 2',
meta_data: null,
rich_text:
'\u003cspan id="LC2" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 2\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_3_3',
type: null,
old_line: 3,
new_line: 3,
text: '- 3',
meta_data: null,
rich_text:
'\u003cspan id="LC3" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 3\u003c/span\u003e\n',
can_receive_suggestion: true,
},
],
},
{
conflict: true,
lines: [
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4_4',
type: 'new',
old_line: null,
new_line: 4,
text: '- 4c',
meta_data: null,
rich_text:
'\u003cspan id="LC4" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 4c\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4_5',
type: 'old',
old_line: 4,
new_line: null,
text: '- 4b',
meta_data: null,
rich_text:
'\u003cspan id="LC4" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 4b\u003c/span\u003e\n',
can_receive_suggestion: false,
},
],
id: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_4_4',
},
{
conflict: false,
lines: [
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_5_5',
type: null,
old_line: 5,
new_line: 5,
text: '- 5',
meta_data: null,
rich_text:
'\u003cspan id="LC5" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 5\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_6_6',
type: null,
old_line: 6,
new_line: 6,
text: '- 6',
meta_data: null,
rich_text:
'\u003cspan id="LC6" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 6\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_7_7',
type: null,
old_line: 7,
new_line: 7,
text: '- 7',
meta_data: null,
rich_text:
'\u003cspan id="LC7" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 7\u003c/span\u003e\n',
can_receive_suggestion: true,
},
],
},
{
conflict: false,
lines: [
{
line_code: null,
type: 'match',
old_line: null,
new_line: null,
text: '@@ -9,15 +9,15 @@',
meta_data: { old_pos: 9, new_pos: 9 },
rich_text: '@@ -9,15 +9,15 @@',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_9_9',
type: null,
old_line: 9,
new_line: 9,
text: '- 9',
meta_data: null,
rich_text:
'\u003cspan id="LC9" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 9\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_10_10',
type: null,
old_line: 10,
new_line: 10,
text: '- 10',
meta_data: null,
rich_text:
'\u003cspan id="LC10" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 10\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_11_11',
type: null,
old_line: 11,
new_line: 11,
text: '- 11',
meta_data: null,
rich_text:
'\u003cspan id="LC11" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 11\u003c/span\u003e\n',
can_receive_suggestion: true,
},
],
},
{
conflict: true,
lines: [
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_12_12',
type: 'new',
old_line: null,
new_line: 12,
text: '- 12c',
meta_data: null,
rich_text:
'\u003cspan id="LC12" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 12c\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_12_13',
type: 'old',
old_line: 12,
new_line: null,
text: '- 12b',
meta_data: null,
rich_text:
'\u003cspan id="LC12" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 12b\u003c/span\u003e\n',
can_receive_suggestion: false,
},
],
id: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_12_12',
},
{
conflict: false,
lines: [
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_13_13',
type: null,
old_line: 13,
new_line: 13,
text: '- 13',
meta_data: null,
rich_text:
'\u003cspan id="LC13" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 13\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_14_14',
type: null,
old_line: 14,
new_line: 14,
text: '- 14 ',
meta_data: null,
rich_text:
'\u003cspan id="LC14" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 14 \u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: '8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d_15_15',
type: null,
old_line: 15,
new_line: 15,
text: '- 15',
meta_data: null,
rich_text:
'\u003cspan id="LC15" class="line" lang="markdown"\u003e\u003cspan class="p"\u003e-\u003c/span\u003e 15\u003c/span\u003e\n',
can_receive_suggestion: true,
},
{
line_code: null,
type: 'match',
old_line: null,
new_line: null,
text: '',
meta_data: { old_pos: 15, new_pos: 15 },
rich_text: '',
can_receive_suggestion: true,
},
],
},
],
type: 'text',
content_path:
'/gitlab-org/gitlab-test/-/merge_requests/2/conflict_for_path?new_path=README.md\u0026old_path=README.md',
},
],
};
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