Commit 197ac5eb authored by Alfredo Sumaran's avatar Alfredo Sumaran

Ability to resolve conflicts for files with `text-editor` as conflict type

parent a8ac9089
...@@ -7,10 +7,10 @@ ...@@ -7,10 +7,10 @@
template: '#diff-file-editor', template: '#diff-file-editor',
data() { data() {
return { return {
originalContent: '',
saved: false, saved: false,
loading: false, loading: false,
fileLoaded: false fileLoaded: false,
originalContent: '',
} }
}, },
computed: { computed: {
...@@ -23,43 +23,48 @@ ...@@ -23,43 +23,48 @@
} }
}, },
watch: { watch: {
loadFile(val) { ['file.showEditor'](val) {
const self = this;
this.resetEditorContent(); this.resetEditorContent();
if (!val || this.fileLoaded || this.loading) { if (!val || this.fileLoaded || this.loading) {
return return;
} }
this.loadEditor();
}
},
ready() {
if (this.file.loadEditor) {
this.loadEditor();
}
},
methods: {
loadEditor() {
this.loading = true; this.loading = true;
$.get(this.file.content_path) $.get(this.file.content_path)
.done((file) => { .done((file) => {
let content = this.$el.querySelector('pre');
let content = self.$el.querySelector('pre');
let fileContent = document.createTextNode(file.content); let fileContent = document.createTextNode(file.content);
content.textContent = fileContent.textContent; content.textContent = fileContent.textContent;
self.originalContent = file.content; this.originalContent = file.content;
self.fileLoaded = true; this.fileLoaded = true;
self.editor = ace.edit(content); this.editor = ace.edit(content);
self.editor.$blockScrolling = Infinity; // Turn off annoying warning this.editor.$blockScrolling = Infinity; // Turn off annoying warning
self.editor.on('change', () => { this.editor.on('change', () => {
self.saveDiffResolution(); this.saveDiffResolution();
}); });
self.saveDiffResolution(); this.saveDiffResolution();
}) })
.fail(() => { .fail(() => {
console.log('error'); console.log('error');
}) })
.always(() => { .always(() => {
self.loading = false; this.loading = false;
}); });
} },
},
methods: {
saveDiffResolution() { saveDiffResolution() {
this.saved = true; this.saved = true;
......
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
INLINE: 'inline', INLINE: 'inline',
PARALLEL: 'parallel' PARALLEL: 'parallel'
}; };
const CONFLICT_TYPES = {
TEXT: 'text',
TEXT_EDITOR: 'text-editor'
};
global.mergeConflicts.mergeConflictsStore = { global.mergeConflicts.mergeConflictsStore = {
state: { state: {
...@@ -26,8 +30,6 @@ ...@@ -26,8 +30,6 @@
setConflictsData(data) { setConflictsData(data) {
this.decorateFiles(data.files); this.decorateFiles(data.files);
this.setInlineLines(data.files);
this.setParallelLines(data.files);
this.state.conflictsData = { this.state.conflictsData = {
files: data.files, files: data.files,
...@@ -45,90 +47,90 @@ ...@@ -45,90 +47,90 @@
file.resolutionData = {}; file.resolutionData = {};
file.promptDiscardConfirmation = false; file.promptDiscardConfirmation = false;
file.resolveMode = DEFAULT_RESOLVE_MODE; file.resolveMode = DEFAULT_RESOLVE_MODE;
}); file.filePath = this.getFilePath(file);
},
setInlineLines(files) {
files.forEach((file) => {
file.iconClass = `fa-${file.blob_icon}`; file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path; file.blobPath = file.blob_path;
file.filePath = this.getFilePath(file);
file.inlineLines = [];
file.sections.forEach((section) => { if (file.type === CONFLICT_TYPES.TEXT) {
let currentLineType = 'new'; file.showEditor = false;
const { conflict, lines, id } = section; file.loadEditor = false;
if (conflict) { this.setInlineLine(file);
file.inlineLines.push(this.getHeadHeaderLine(id)); this.setParallelLine(file);
} } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) {
file.showEditor = true;
file.loadEditor = true;
}
});
},
lines.forEach((line) => { setInlineLine(file) {
const { type } = line; file.inlineLines = [];
if ((type === 'new' || type === 'old') && currentLineType !== type) { file.sections.forEach((section) => {
currentLineType = type; let currentLineType = 'new';
file.inlineLines.push({ lineType: 'emptyLine', richText: '' }); const { conflict, lines, id } = section;
}
if (conflict) {
file.inlineLines.push(this.getHeadHeaderLine(id));
}
this.decorateLineForInlineView(line, id, conflict); lines.forEach((line) => {
file.inlineLines.push(line); const { type } = line;
})
if (conflict) { if ((type === 'new' || type === 'old') && currentLineType !== type) {
file.inlineLines.push(this.getOriginHeaderLine(id)); 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));
}
}); });
}, },
setParallelLines(files) { setParallelLine(file) {
files.forEach((file) => { file.parallelLines = [];
file.filePath = this.getFilePath(file); const linesObj = { left: [], right: [] };
file.iconClass = `fa-${file.blob_icon}`;
file.blobPath = file.blob_path;
file.parallelLines = [];
const linesObj = { left: [], right: [] };
file.sections.forEach((section) => {
const { conflict, lines, id } = section;
if (conflict) { file.sections.forEach((section) => {
linesObj.left.push(this.getOriginHeaderLine(id)); const { conflict, lines, id } = section;
linesObj.right.push(this.getHeadHeaderLine(id));
}
lines.forEach((line) => { if (conflict) {
const { type } = line; linesObj.left.push(this.getOriginHeaderLine(id));
linesObj.right.push(this.getHeadHeaderLine(id));
}
if (conflict) { lines.forEach((line) => {
if (type === 'old') { const { type } = line;
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)); if (conflict) {
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true)); 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';
this.checkLineLengths(linesObj); linesObj.left.push (this.getLineForParallelView(line, id, lineType));
linesObj.right.push(this.getLineForParallelView(line, id, lineType, true));
}
}); });
for (let i = 0, len = linesObj.left.length; i < len; i++) { this.checkLineLengths(linesObj);
file.parallelLines.push([
linesObj.right[i],
linesObj.left[i]
]);
}
return file;
}); });
for (let i = 0, len = linesObj.left.length; i < len; i++) {
file.parallelLines.push([
linesObj.right[i],
linesObj.left[i]
]);
}
}, },
setLoadingState(state) { setLoadingState(state) {
...@@ -140,13 +142,12 @@ ...@@ -140,13 +142,12 @@
}, },
setFailedRequest(message) { setFailedRequest(message) {
console.log('setFailedRequest');
this.state.hasError = true; this.state.hasError = true;
this.state.conflictsData.errorMessage = message; this.state.conflictsData.errorMessage = message;
}, },
getConflictsCount() { getConflictsCount() {
if (!this.state.conflictsData.files) { if (!this.state.conflictsData.files.length) {
return 0; return 0;
} }
...@@ -154,11 +155,15 @@ ...@@ -154,11 +155,15 @@
let count = 0; let count = 0;
files.forEach((file) => { files.forEach((file) => {
file.sections.forEach((section) => { if (file.type === CONFLICT_TYPES.TEXT) {
if (section.conflict) { file.sections.forEach((section) => {
count++; if (section.conflict) {
} count++;
}); }
});
} else {
count++;
}
}); });
return count; return count;
...@@ -172,7 +177,7 @@ ...@@ -172,7 +177,7 @@
}, },
setViewType(viewType) { setViewType(viewType) {
this.state.diffView = viewType; this.state.diffView = viewType;
this.state.isParallel = viewType === VIEW_TYPES.PARALLEL; this.state.isParallel = viewType === VIEW_TYPES.PARALLEL;
$.cookie('diff_view', viewType, { $.cookie('diff_view', viewType, {
...@@ -253,8 +258,7 @@ ...@@ -253,8 +258,7 @@
for (let i = 0; i < diff; i++) { for (let i = 0; i < diff; i++) {
right.push({ lineType: 'emptyLine', richText: '' }); right.push({ lineType: 'emptyLine', richText: '' });
} }
} } else {
else {
const diff = right.length - left.length; const diff = right.length - left.length;
for (let i = 0; i < diff; i++) { for (let i = 0; i < diff; i++) {
left.push({ lineType: 'emptyLine', richText: '' }); left.push({ lineType: 'emptyLine', richText: '' });
...@@ -268,8 +272,12 @@ ...@@ -268,8 +272,12 @@
}, },
setFileResolveMode(file, mode) { setFileResolveMode(file, mode) {
// Restore Interactive mode when switching to Edit mode if (mode === INTERACTIVE_RESOLVE_MODE) {
if (mode === EDIT_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 = {}; file.resolutionData = {};
this.restoreFileLinesState(file); this.restoreFileLinesState(file);
...@@ -287,9 +295,9 @@ ...@@ -287,9 +295,9 @@
}); });
file.parallelLines.forEach((lines) => { file.parallelLines.forEach((lines) => {
const left = lines[0]; const left = lines[0];
const right = lines[1]; const right = lines[1];
const isLeftMatch = left.hasConflict || left.isHeader; const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader; const isRightMatch = right.hasConflict || right.isHeader;
if (isLeftMatch || isRightMatch) { if (isLeftMatch || isRightMatch) {
...@@ -313,14 +321,17 @@ ...@@ -313,14 +321,17 @@
let numberConflicts = 0; let numberConflicts = 0;
let resolvedConflicts = Object.keys(file.resolutionData).length let resolvedConflicts = Object.keys(file.resolutionData).length
for (let j = 0, k = file.sections.length; j < k; j++) { // We only check if
if (file.sections[j].conflict) { if (file.type === CONFLICT_TYPES.TEXT) {
numberConflicts++; for (let j = 0, k = file.sections.length; j < k; j++) {
if (file.sections[j].conflict) {
numberConflicts++;
}
} }
}
if (resolvedConflicts !== numberConflicts) { if (resolvedConflicts !== numberConflicts) {
unresolved++; unresolved++;
}
} }
} else if (file.resolveMode === EDIT_RESOLVE_MODE) { } else if (file.resolveMode === EDIT_RESOLVE_MODE) {
// Unlikely to happen since switching to Edit mode saves content automatically. // Unlikely to happen since switching to Edit mode saves content automatically.
...@@ -358,10 +369,15 @@ ...@@ -358,10 +369,15 @@
new_path: file.new_path new_path: file.new_path
}; };
// Submit only one data for type of editing if (file.type === CONFLICT_TYPES.TEXT) {
if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
addFile.sections = file.resolutionData; // Submit only one data for type of editing
} else if (file.resolveMode === EDIT_RESOLVE_MODE) { 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; addFile.content = file.content;
} }
...@@ -374,39 +390,35 @@ ...@@ -374,39 +390,35 @@
handleSelected(file, sectionId, selection) { handleSelected(file, sectionId, selection) {
Vue.set(file.resolutionData, sectionId, selection); Vue.set(file.resolutionData, sectionId, selection);
this.state.conflictsData.files.forEach((file) => { file.inlineLines.forEach((line) => {
file.inlineLines.forEach((line) => { if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
if (line.id === sectionId && (line.hasConflict || line.isHeader)) { this.markLine(line, selection);
this.markLine(line, selection); }
} });
});
file.parallelLines.forEach((lines) => { file.parallelLines.forEach((lines) => {
const left = lines[0]; const left = lines[0];
const right = lines[1]; const right = lines[1];
const hasSameId = right.id === sectionId || left.id === sectionId; const hasSameId = right.id === sectionId || left.id === sectionId;
const isLeftMatch = left.hasConflict || left.isHeader; const isLeftMatch = left.hasConflict || left.isHeader;
const isRightMatch = right.hasConflict || right.isHeader; const isRightMatch = right.hasConflict || right.isHeader;
if (hasSameId && (isLeftMatch || isRightMatch)) { if (hasSameId && (isLeftMatch || isRightMatch)) {
this.markLine(left, selection); this.markLine(left, selection);
this.markLine(right, selection); this.markLine(right, selection);
} }
})
}); });
}, },
markLine(line, selection) { markLine(line, selection) {
if (selection === 'head' && line.isHead) { if (selection === 'head' && line.isHead) {
line.isSelected = true; line.isSelected = true;
line.isUnselected = false; line.isUnselected = false;
} } else if (selection === 'origin' && line.isOrigin) {
else if (selection === 'origin' && line.isOrigin) { line.isSelected = true;
line.isSelected = true;
line.isUnselected = false; line.isUnselected = false;
} } else {
else { line.isSelected = false;
line.isSelected = false;
line.isUnselected = true; line.isUnselected = true;
} }
}, },
......
- if_condition = local_assigns.fetch(:if_condition, '') .diff-editor-wrap{ "v-show" => "file.showEditor" }
.diff-editor-wrap{ "v-show" => if_condition }
.discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" } .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" }
.discard-changes-alert .discard-changes-alert
Are you sure to discard your changes? Are you sure to discard your changes?
.discard-actions .discard-actions
%button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes %button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes
%button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel
%diff-file-editor{":file" => "file", ":load-file" => if_condition } %diff-file-editor{":file" => "file"}
.file-actions .file-actions
.btn-group .btn-group{"v-if" => "file.type === 'text'"}
%button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }", %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
'@click' => "onClickResolveModeButton(file, 'interactive')", '@click' => "onClickResolveModeButton(file, 'interactive')",
type: 'button' } type: 'button' }
......
...@@ -4,23 +4,26 @@ ...@@ -4,23 +4,26 @@
%i.fa.fa-fw{":class" => "file.iconClass"} %i.fa.fa-fw{":class" => "file.iconClass"}
%strong {{file.filePath}} %strong {{file.filePath}}
= render partial: 'projects/merge_requests/conflicts/file_actions' = render partial: 'projects/merge_requests/conflicts/file_actions'
.diff-content.diff-wrap-lines %template{"v-if" => "file.type === 'text'"}
.diff-wrap-lines.code.file-content.js-syntax-highlight{ 'v-show' => "file.resolveMode === 'interactive'" } .diff-content.diff-wrap-lines
%table .diff-wrap-lines.code.file-content.js-syntax-highlight{ 'v-show' => "file.resolveMode === 'interactive'" }
%tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"} %table
%template{"v-if" => "!line.isHeader"} %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
%td.diff-line-num.new_line{":class" => class_bindings} %template{"v-if" => "!line.isHeader"}
%a {{line.new_line}} %td.diff-line-num.new_line{":class" => class_bindings}
%td.diff-line-num.old_line{":class" => class_bindings} %a {{line.new_line}}
%a {{line.old_line}} %td.diff-line-num.old_line{":class" => class_bindings}
%td.line_content{":class" => class_bindings} %a {{line.old_line}}
{{{line.richText}}} %td.line_content{":class" => class_bindings}
{{{line.richText}}}
%template{"v-if" => "line.isHeader"} %template{"v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => class_bindings} %td.diff-line-num.header{":class" => class_bindings}
%td.diff-line-num.header{":class" => class_bindings} %td.diff-line-num.header{":class" => class_bindings}
%td.line_content.header{":class" => class_bindings} %td.line_content.header{":class" => class_bindings}
%strong {{{line.richText}}} %strong {{{line.richText}}}
%button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
{{line.buttonTitle}} {{line.buttonTitle}}
= render partial: 'projects/merge_requests/conflicts/diff_file_editor', locals: { if_condition: "file.resolveMode === 'edit' && !isParallel" } = render partial: 'projects/merge_requests/conflicts/diff_file_editor'
%template{"v-else" => true}
= render partial: 'projects/merge_requests/conflicts/diff_file_editor'
...@@ -22,4 +22,4 @@ ...@@ -22,4 +22,4 @@
{{line.lineNumber}} {{line.lineNumber}}
%td.line_content.parallel{":class" => class_bindings} %td.line_content.parallel{":class" => class_bindings}
{{{line.richText}}} {{{line.richText}}}
= render partial: 'projects/merge_requests/conflicts/diff_file_editor', locals: { if_condition: "file.resolveMode === 'edit' && isParallel" } = render partial: 'projects/merge_requests/conflicts/diff_file_editor', locals: { if_condition: "file.loadFile && isParallel" }
...@@ -37,7 +37,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do ...@@ -37,7 +37,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do
end end
end end
context 'when in inline mode' do context 'when in inline mode' do
it 'resolves files manually' do it 'resolves files manually' do
within find('.files-wrapper .diff-file.inline-view', text: 'files/ruby/popen.rb') do within find('.files-wrapper .diff-file.inline-view', text: 'files/ruby/popen.rb') do
click_button 'Edit inline' click_button 'Edit inline'
...@@ -66,6 +66,42 @@ feature 'Merge request conflict resolution', js: true, feature: true do ...@@ -66,6 +66,42 @@ feature 'Merge request conflict resolution', js: true, feature: true do
end end
end end
context 'when a merge request can be resolved in the UI' do
let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') }
before do
project.team << [user, :developer]
login_as(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
context 'a conflict contain markers' do
before { click_link('conflicts', href: /\/conflicts\Z/) }
it 'resolves files manually' do
within find('.files-wrapper .diff-file.inline-view', text: 'files/markdown/ruby-style-guide.md') do
wait_for_ajax
execute_script('ace.edit($(".files-wrapper .diff-file.inline-view pre")[0]).setValue("Gregor Samsa woke from troubled dreams");')
end
click_button 'Commit conflict resolution'
wait_for_ajax
expect(page).to have_content('All merge conflicts were resolved')
merge_request.reload_diff
click_on 'Changes'
wait_for_ajax
find('.nothing-here-block', visible: true).click
wait_for_ajax
expect(page).to have_content('Gregor Samsa woke from troubled dreams')
end
end
end
UNRESOLVABLE_CONFLICTS = { UNRESOLVABLE_CONFLICTS = {
'conflict-too-large' => 'when the conflicts contain a large file', 'conflict-too-large' => 'when the conflicts contain a large file',
'conflict-binary-file' => 'when the conflicts contain a binary file', 'conflict-binary-file' => 'when the conflicts contain a binary file',
......
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