summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlfredo Sumaran <alfredo@gitlab.com>2016-09-08 11:44:04 -0500
committerAlfredo Sumaran <alfredo@gitlab.com>2016-10-13 14:16:34 -0500
commit26f658decd3943bbc66c567ea91e7b859b32e0e6 (patch)
treef34f505c10259d457a6bf22d446fe5ada6ca9cf2
parent6af52d7d23cf9dbfcd58a2d3031ed19887f7a558 (diff)
downloadgitlab-ce-26f658decd3943bbc66c567ea91e7b859b32e0e6.tar.gz
Implement editor to manually resolve merge conflicts
-rw-r--r--app/assets/javascripts/merge_conflict_data_provider.js.es6165
-rw-r--r--app/assets/javascripts/merge_conflict_resolver.js.es660
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es663
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss26
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml3
-rw-r--r--app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml9
-rw-r--r--app/views/projects/merge_requests/conflicts/_file_actions.html.haml12
-rw-r--r--app/views/projects/merge_requests/conflicts/_inline_view.html.haml10
-rw-r--r--app/views/projects/merge_requests/conflicts/_parallel_view.html.haml10
-rw-r--r--app/views/projects/merge_requests/conflicts/_submit_form.html.haml9
-rw-r--r--app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml6
11 files changed, 300 insertions, 73 deletions
diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6
index 13ee794ba38..bfed78ff99c 100644
--- a/app/assets/javascripts/merge_conflict_data_provider.js.es6
+++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6
@@ -2,7 +2,9 @@ const HEAD_HEADER_TEXT = 'HEAD//our changes';
const ORIGIN_HEADER_TEXT = 'origin//their changes';
const HEAD_BUTTON_TITLE = 'Use ours';
const ORIGIN_BUTTON_TITLE = 'Use theirs';
-
+const INTERACTIVE_RESOLVE_MODE = 'interactive';
+const EDIT_RESOLVE_MODE = 'edit';
+const DEFAULT_RESOLVE_MODE = INTERACTIVE_RESOLVE_MODE;
class MergeConflictDataProvider {
@@ -18,8 +20,7 @@ class MergeConflictDataProvider {
diffViewType : diffViewType,
fixedLayout : fixedLayout,
isSubmitting : false,
- conflictsData : {},
- resolutionData : {}
+ conflictsData : {}
}
}
@@ -35,9 +36,9 @@ class MergeConflictDataProvider {
data.shortCommitSha = data.commit_sha.slice(0, 7);
data.commitMessage = data.commit_message;
+ this.decorateFiles(data);
this.setParallelLines(data);
this.setInlineLines(data);
- this.updateResolutionsData(data);
}
vueInstance.conflictsData = data;
@@ -47,16 +48,12 @@ class MergeConflictDataProvider {
vueInstance.conflictsData.conflictsText = conflictsText;
}
-
- updateResolutionsData(data) {
- const vi = this.vueInstance;
-
- data.files.forEach( (file) => {
- file.sections.forEach( (section) => {
- if (section.conflict) {
- vi.$set(`resolutionData['${section.id}']`, false);
- }
- });
+ decorateFiles(data) {
+ data.files.forEach((file) => {
+ file.content = '';
+ file.resolutionData = {};
+ file.promptDiscardConfirmation = false;
+ file.resolveMode = DEFAULT_RESOLVE_MODE;
});
}
@@ -165,11 +162,14 @@ class MergeConflictDataProvider {
}
- handleSelected(sectionId, selection) {
+ handleSelected(file, sectionId, selection) {
const vi = this.vueInstance;
+ let files = vi.conflictsData.files;
+
+ vi.$set(`conflictsData.files[${files.indexOf(file)}].resolutionData['${sectionId}']`, selection);
+
- vi.resolutionData[sectionId] = selection;
- vi.conflictsData.files.forEach( (file) => {
+ files.forEach( (file) => {
file.inlineLines.forEach( (line) => {
if (line.id === sectionId && (line.hasConflict || line.isHeader)) {
this.markLine(line, selection);
@@ -208,6 +208,48 @@ class MergeConflictDataProvider {
.toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
}
+ setFileResolveMode(file, mode) {
+ const vi = this.vueInstance;
+
+ // Restore Interactive mode when switching to Edit mode
+ if (mode === EDIT_RESOLVE_MODE) {
+ 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;
+ }
+ });
+ }
+
+
+ setPromptConfirmationState(file, state) {
+ file.promptDiscardConfirmation = state;
+ }
+
markLine(line, selection) {
if (selection === 'head' && line.isHead) {
@@ -226,31 +268,54 @@ class MergeConflictDataProvider {
getConflictsCount() {
- return Object.keys(this.vueInstance.resolutionData).length;
- }
-
-
- getResolvedCount() {
- let count = 0;
- const data = this.vueInstance.resolutionData;
+ const files = this.vueInstance.conflictsData.files;
+ let count = 0;
- for (const id in data) {
- const resolution = data[id];
- if (resolution) {
- count++;
- }
- }
+ files.forEach((file) => {
+ file.sections.forEach((section) => {
+ if (section.conflict) {
+ count++;
+ }
+ });
+ });
return count;
}
isReadyToCommit() {
- const { conflictsData, isSubmitting } = this.vueInstance
- const allResolved = this.getConflictsCount() === this.getResolvedCount();
- const hasCommitMessage = $.trim(conflictsData.commitMessage).length;
+ const vi = this.vueInstance;
+ const files = this.vueInstance.conflictsData.files;
+ const hasCommitMessage = $.trim(this.vueInstance.conflictsData.commitMessage).length;
+ let unresolved = 0;
+
+ for (let i = 0, l = files.length; i < l; i++) {
+ let file = files[i];
+
+ if (file.resolveMode === INTERACTIVE_RESOLVE_MODE) {
+ let numberConflicts = 0;
+ let resolvedConflicts = Object.keys(file.resolutionData).length
+
+ for (let j = 0, k = file.sections.length; j < k; j++) {
+ if (file.sections[j].conflict) {
+ numberConflicts++;
+ }
+ }
+
+ if (resolvedConflicts !== numberConflicts) {
+ unresolved++;
+ }
+ } 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++;
+ continue;
+ }
+ }
+ }
- return !isSubmitting && hasCommitMessage && allResolved;
+ return !vi.isSubmitting && hasCommitMessage && !unresolved;
}
@@ -332,10 +397,33 @@ class MergeConflictDataProvider {
getCommitData() {
- return {
- commit_message: this.vueInstance.conflictsData.commitMessage,
- sections: this.vueInstance.resolutionData
- }
+ let conflictsData = this.vueInstance.conflictsData;
+ let commitData = {};
+
+ commitData = {
+ commitMessage: conflictsData.commitMessage,
+ files: []
+ };
+
+ conflictsData.files.forEach((file) => {
+ let addFile;
+
+ addFile = {
+ old_path: file.old_path,
+ new_path: file.new_path
+ };
+
+ // 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;
+ }
+
+ commitData.files.push(addFile);
+ });
+
+ return commitData;
}
@@ -343,5 +431,4 @@ class MergeConflictDataProvider {
const { old_path, new_path } = file;
return old_path === new_path ? new_path : `${old_path} → ${new_path}`;
}
-
}
diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6
index 7e756433bf5..c317a6521b5 100644
--- a/app/assets/javascripts/merge_conflict_resolver.js.es6
+++ b/app/assets/javascripts/merge_conflict_resolver.js.es6
@@ -1,13 +1,16 @@
//= require vue
+//= require ./merge_conflicts/components/diff_file_editor
+
+const INTERACTIVE_RESOLVE_MODE = 'interactive';
+const EDIT_RESOLVE_MODE = 'edit';
class MergeConflictResolver {
constructor() {
- this.dataProvider = new MergeConflictDataProvider()
- this.initVue()
+ this.dataProvider = new MergeConflictDataProvider();
+ this.initVue();
}
-
initVue() {
const that = this;
this.vue = new Vue({
@@ -17,15 +20,28 @@ class MergeConflictResolver {
created : this.fetchData(),
computed : this.setComputedProperties(),
methods : {
- handleSelected(sectionId, selection) {
- that.dataProvider.handleSelected(sectionId, selection);
+ handleSelected(file, sectionId, selection) {
+ that.dataProvider.handleSelected(file, sectionId, selection);
},
handleViewTypeChange(newType) {
that.dataProvider.updateViewType(newType);
},
commit() {
that.commit();
- }
+ },
+ onClickResolveModeButton(file, mode) {
+ that.toggleResolveMode(file, mode);
+ },
+ acceptDiscardConfirmation(file) {
+ that.dataProvider.setPromptConfirmationState(file, false);
+ that.dataProvider.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE);
+ },
+ cancelDiscardConfirmation(file) {
+ that.dataProvider.setPromptConfirmationState(file, false);
+ },
+ },
+ components: {
+ 'diff-file-editor': window.gl.diffFileEditor
}
})
}
@@ -36,7 +52,6 @@ class MergeConflictResolver {
return {
conflictsCount() { return dp.getConflictsCount() },
- resolvedCount() { return dp.getResolvedCount() },
readyToCommit() { return dp.isReadyToCommit() },
commitButtonText() { return dp.getCommitButtonText() }
}
@@ -69,14 +84,29 @@ class MergeConflictResolver {
commit() {
this.vue.isSubmitting = true;
- $.post($('#conflicts').data('resolveConflictsPath'), this.dataProvider.getCommitData())
- .done((data) => {
- window.location.href = data.redirect_to;
- })
- .error(() => {
- this.vue.isSubmitting = false;
- new Flash('Something went wrong!');
- });
+ $.ajax({
+ url: $('#conflicts').data('resolveConflictsPath'),
+ data: JSON.stringify(this.dataProvider.getCommitData()),
+ contentType: "application/json",
+ dataType: 'json',
+ method: 'POST'
+ })
+ .done((data) => {
+ window.location.href = data.redirect_to;
+ })
+ .error(() => {
+ this.vue.isSubmitting = false;
+ new Flash('Something went wrong!');
+ });
}
+
+ toggleResolveMode(file, mode) {
+ if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) {
+ this.dataProvider.setPromptConfirmationState(file, true);
+ return;
+ }
+
+ this.dataProvider.setFileResolveMode(file, mode);
+ }
}
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
new file mode 100644
index 00000000000..570d9ff877c
--- /dev/null
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -0,0 +1,63 @@
+((global) => {
+ global.diffFileEditor = Vue.extend({
+ props: ['file', 'loadFile'],
+ template: '#diff-file-editor',
+ data() {
+ return {
+ originalState: '',
+ saved: false,
+ loading: false,
+ fileLoaded: false
+ }
+ },
+ computed: {
+ classObject() {
+ return {
+ 'load-file': this.loadFile,
+ 'saved': this.saved,
+ 'is-loading': this.loading
+ };
+ }
+ },
+ watch: {
+ loadFile(val) {
+ const self = this;
+
+ if (!val || this.fileLoaded || this.loading) {
+ return
+ }
+
+ this.loading = true;
+
+ $.get(this.file.content_path)
+ .done((file) => {
+ $(self.$el).find('textarea').val(file.content);
+
+ self.originalState = file.content;
+ self.fileLoaded = true;
+ self.saveDiffResolution();
+ })
+ .fail(() => {
+ console.log('error');
+ })
+ .always(() => {
+ self.loading = false;
+ });
+ }
+ },
+ methods: {
+ saveDiffResolution() {
+ this.saved = true;
+
+ // This probably be better placed in the data provider
+ this.file.content = this.$el.querySelector('textarea').value;
+ this.file.resolveEditChanged = this.file.content !== this.originalState;
+ this.file.promptDiscardConfirmation = false;
+ },
+ onInput() {
+ this.saveDiffResolution();
+ }
+ }
+ });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 5ec660799e3..577a97e8c0e 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -235,4 +235,30 @@ $colors: (
.btn-success .fa-spinner {
color: #fff;
}
+
+ .editor-wrap {
+ &.is-loading {
+ .editor {
+ display: none;
+ }
+
+ .loading-text {
+ display: block;
+ }
+ }
+
+ &.saved {
+ .editor {
+ border-top: solid 1px green;
+ }
+ }
+
+ .editor {
+ border-top: solid 1px yellow;
+ }
+
+ .loading-text {
+ display: none;
+ }
+ }
}
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index a524936f73c..e3add1f799f 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -27,3 +27,6 @@
= render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings }
= render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings }
= render partial: "projects/merge_requests/conflicts/submit_form"
+
+-# Components
+= render partial: 'projects/merge_requests/conflicts/components/diff_file_editor'
diff --git a/app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml
new file mode 100644
index 00000000000..d0c518e5249
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml
@@ -0,0 +1,9 @@
+- if_condition = local_assigns.fetch(:if_condition, '')
+
+.diff-editor-wrap{ "v-show" => if_condition }
+ .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" }
+ %p
+ Are you sure to discard your changes?
+ %button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes
+ %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel
+ %diff-file-editor{":file" => "file", ":load-file" => if_condition }
diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
new file mode 100644
index 00000000000..124dfde7b22
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml
@@ -0,0 +1,12 @@
+.file-actions
+ .btn-group
+ %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }",
+ '@click' => "onClickResolveModeButton(file, 'interactive')",
+ type: 'button' }
+ Interactive mode
+ %button.btn{ ':class' => "{ 'active': file.resolveMode == 'edit' }",
+ '@click' => "onClickResolveModeButton(file, 'edit')",
+ type: 'button' }
+ Edit inline
+ %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
+ View file @{{conflictsData.shortCommitSha}}
diff --git a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
index 19c7da4b5e3..7120b6ff48d 100644
--- a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml
@@ -3,12 +3,9 @@
.file-title
%i.fa.fa-fw{":class" => "file.iconClass"}
%strong {{file.filePath}}
- .file-actions
- %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
- View file @{{conflictsData.shortCommitSha}}
-
+ = render partial: 'projects/merge_requests/conflicts/file_actions'
.diff-content.diff-wrap-lines
- .diff-wrap-lines.code.file-content.js-syntax-highlight
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{ 'v-show' => "file.resolveMode === 'interactive'" }
%table
%tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"}
%template{"v-if" => "!line.isHeader"}
@@ -24,5 +21,6 @@
%td.diff-line-num.header{":class" => class_bindings}
%td.line_content.header{":class" => class_bindings}
%strong {{{line.richText}}}
- %button.btn{"@click" => "handleSelected(line.id, line.section)"}
+ %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
{{line.buttonTitle}}
+ = render partial: 'projects/merge_requests/conflicts/diff_file_editor', locals: { if_condition: "file.resolveMode === 'edit' && !isParallel" }
diff --git a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
index 2e6f67c2eaf..18c830ddb17 100644
--- a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
@@ -3,12 +3,9 @@
.file-title
%i.fa.fa-fw{":class" => "file.iconClass"}
%strong {{file.filePath}}
- .file-actions
- %a.btn.view-file.btn-file-option{":href" => "file.blobPath"}
- View file @{{conflictsData.shortCommitSha}}
-
+ = render partial: 'projects/merge_requests/conflicts/file_actions'
.diff-content.diff-wrap-lines
- .diff-wrap-lines.code.file-content.js-syntax-highlight
+ .diff-wrap-lines.code.file-content.js-syntax-highlight{ 'v-show' => "file.resolveMode === 'interactive'" }
%table
%tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
%template{"v-for" => "line in section"}
@@ -17,7 +14,7 @@
%td.diff-line-num.header{":class" => class_bindings}
%td.line_content.header{":class" => class_bindings}
%strong {{line.richText}}
- %button.btn{"@click" => "handleSelected(line.id, line.section)"}
+ %button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
{{line.buttonTitle}}
%template{"v-if" => "!line.isHeader"}
@@ -25,3 +22,4 @@
{{line.lineNumber}}
%td.line_content.parallel{":class" => class_bindings}
{{{line.richText}}}
+ = render partial: 'projects/merge_requests/conflicts/diff_file_editor', locals: { if_condition: "file.resolveMode === 'edit' && isParallel" }
diff --git a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
index 78bd4133ea2..380f8722186 100644
--- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
+++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml
@@ -1,15 +1,10 @@
-.content-block.oneline-block.files-changed
- %strong.resolved-count {{resolvedCount}}
- of
- %strong.total-count {{conflictsCount}}
- conflicts have been resolved
-
+.content-block
.commit-message-container.form-group
.max-width-marker
%textarea.form-control.js-commit-message{"v-model" => "conflictsData.commitMessage"}
{{{conflictsData.commitMessage}}}
- %button{type: "button", class: "btn btn-success js-submit-button", ":disabled" => "!readyToCommit", "@click" => "commit()"}
+ %button{type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" }
%span {{commitButtonText}}
= link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel"
diff --git a/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
new file mode 100644
index 00000000000..0556341fd64
--- /dev/null
+++ b/app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
@@ -0,0 +1,6 @@
+%template{ id: "diff-file-editor" }
+ %div
+ .editor-wrap{ ":class" => "classObject" }
+ %p.loading-text Loading...
+ .editor
+ %textarea{ "@input" => "onInput", cols: '80', rows: '20' }