Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
Jérome Perrin
gitlab-ce
Commits
26f658de
Commit
26f658de
authored
Sep 08, 2016
by
Alfredo Sumaran
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement editor to manually resolve merge conflicts
parent
6af52d7d
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
300 additions
and
73 deletions
+300
-73
app/assets/javascripts/merge_conflict_data_provider.js.es6
app/assets/javascripts/merge_conflict_data_provider.js.es6
+126
-39
app/assets/javascripts/merge_conflict_resolver.js.es6
app/assets/javascripts/merge_conflict_resolver.js.es6
+45
-15
app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
...cripts/merge_conflicts/components/diff_file_editor.js.es6
+63
-0
app/assets/stylesheets/pages/merge_conflicts.scss
app/assets/stylesheets/pages/merge_conflicts.scss
+26
-0
app/views/projects/merge_requests/conflicts.html.haml
app/views/projects/merge_requests/conflicts.html.haml
+3
-0
app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml
...ects/merge_requests/conflicts/_diff_file_editor.html.haml
+9
-0
app/views/projects/merge_requests/conflicts/_file_actions.html.haml
...projects/merge_requests/conflicts/_file_actions.html.haml
+12
-0
app/views/projects/merge_requests/conflicts/_inline_view.html.haml
.../projects/merge_requests/conflicts/_inline_view.html.haml
+4
-6
app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
...rojects/merge_requests/conflicts/_parallel_view.html.haml
+4
-6
app/views/projects/merge_requests/conflicts/_submit_form.html.haml
.../projects/merge_requests/conflicts/_submit_form.html.haml
+2
-7
app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
...requests/conflicts/components/_diff_file_editor.html.haml
+6
-0
No files found.
app/assets/javascripts/merge_conflict_data_provider.js.es6
View file @
26f658de
...
...
@@ -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 && allR
esolved;
return !
vi.isSubmitting && hasCommitMessage && !unr
esolved;
}
...
...
@@ -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}`;
}
}
app/assets/javascripts/merge_conflict_resolver.js.es6
View file @
26f658de
//= 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);
}
}
app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
0 → 100644
View file @
26f658de
((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 = {}));
app/assets/stylesheets/pages/merge_conflicts.scss
View file @
26f658de
...
...
@@ -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
;
}
}
}
app/views/projects/merge_requests/conflicts.html.haml
View file @
26f658de
...
...
@@ -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'
app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml
0 → 100644
View file @
26f658de
-
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
}
app/views/projects/merge_requests/conflicts/_file_actions.html.haml
0 → 100644
View file @
26f658de
.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}}
app/views/projects/merge_requests/conflicts/_inline_view.html.haml
View file @
26f658de
...
...
@@ -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"
}
app/views/projects/merge_requests/conflicts/_parallel_view.html.haml
View file @
26f658de
...
...
@@ -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"
}
app/views/projects/merge_requests/conflicts/_submit_form.html.haml
View file @
26f658de
.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"
app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml
0 → 100644
View file @
26f658de
%template
{
id:
"diff-file-editor"
}
%div
.editor-wrap
{
":class"
=>
"classObject"
}
%p
.loading-text
Loading...
.editor
%textarea
{
"@input"
=>
"onInput"
,
cols:
'80'
,
rows:
'20'
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment