Commit df2ed097 authored by Sean McGivern's avatar Sean McGivern Committed by Fatih Acet

Add backend for merge conflicts reading

parent ff15fbf1
...@@ -21,9 +21,9 @@ class window.MergeConflictResolver extends Vue ...@@ -21,9 +21,9 @@ class window.MergeConflictResolver extends Vue
fetchData: -> fetchData: ->
$.ajax $.ajax
url : '/emojis' url : './conflicts.json'
success: (response) => success: (response) =>
@handleResponse window.mergeConflictsData @handleResponse response
handleResponse: (data) -> handleResponse: (data) ->
...@@ -81,6 +81,8 @@ class window.MergeConflictResolver extends Vue ...@@ -81,6 +81,8 @@ class window.MergeConflictResolver extends Vue
headHeaderText = 'HEAD//our changes' headHeaderText = 'HEAD//our changes'
originHeaderText = 'origin//their changes' originHeaderText = 'origin//their changes'
data.shortCommitSha = data.commit_sha.slice 0, 7
data.commitMessage = data.commit_message
@updateResolutionsData data @updateResolutionsData data
...@@ -88,7 +90,6 @@ class window.MergeConflictResolver extends Vue ...@@ -88,7 +90,6 @@ class window.MergeConflictResolver extends Vue
for file in data.files for file in data.files
file.parallelLines = { left: [], right: [] } file.parallelLines = { left: [], right: [] }
file.inlineLines = [] file.inlineLines = []
file.shortCommitSha = file.commit_sha.slice 0, 7
currentLineType = 'old' currentLineType = 'old'
for section in file.sections for section in file.sections
...@@ -96,41 +97,42 @@ class window.MergeConflictResolver extends Vue ...@@ -96,41 +97,42 @@ class window.MergeConflictResolver extends Vue
if conflict if conflict
# FIXME: Make these lines better # FIXME: Make these lines better
file.parallelLines.left.push { isHeader: yes, id, text: headHeaderText, section: 'head', isHead: yes, isSelected: no, isUnselected: no } file.parallelLines.left.push { isHeader: yes, id, richText: headHeaderText, section: 'head', isHead: yes, isSelected: no, isUnselected: no }
file.parallelLines.right.push { isHeader: yes, id, text: originHeaderText, section: 'origin', isOrigin: yes, isSelected: no, isUnselected: no } file.parallelLines.right.push { isHeader: yes, id, richText: originHeaderText, section: 'origin', isOrigin: yes, isSelected: no, isUnselected: no }
file.inlineLines.push { isHeader: yes, id, text: headHeaderText, type: 'old', section: 'head', isHead: yes, isSelected: no, isUnselected: no } file.inlineLines.push { isHeader: yes, id, richText: headHeaderText, type: 'old', section: 'head', isHead: yes, isSelected: no, isUnselected: no }
for line in lines for line in lines
if line.type in ['new', 'old'] and currentLineType isnt line.type if line.type in ['new', 'old'] and currentLineType isnt line.type
currentLineType = line.type currentLineType = line.type
# FIXME: Find a better way to add a new line # FIXME: Find a better way to add a new line
file.inlineLines.push { lineType: 'emptyLine', text: '<span> </span>' } file.inlineLines.push { lineType: 'emptyLine', richText: '<span> </span>' }
# FIXME: Make these lines better # FIXME: Make these lines better
line.conflict = conflict line.conflict = conflict
line.id = id line.id = id
line.isHead = line.type is 'old' line.isHead = line.type is 'new'
line.isOrigin = line.type is 'new' line.isOrigin = line.type is 'old'
line.isSelected = no line.isSelected = no
line.isUnselected = no line.isUnselected = no
line.richText = line.rich_text
file.inlineLines.push line file.inlineLines.push line
if conflict if conflict
if line.type is 'old' if line.type is 'old'
line = { lineType: 'conflict', lineNumber: line.old_line, text: line.text, section: 'head', id, isSelected: no, isUnselected: no, isHead: yes } line = { lineType: 'conflict', lineNumber: line.old_line, richText: line.rich_text, section: 'origin', id, isSelected: no, isUnselected: no, isOrigin: yes }
file.parallelLines.left.push line file.parallelLines.left.push line
else if line.type is 'new' else if line.type is 'new'
line = { lineType: 'conflict', lineNumber: line.new_line, text: line.text, section: 'origin', id, isSelected: no, isUnselected: no, isOrigin: yes } line = { lineType: 'conflict', lineNumber: line.new_line, richText: line.rich_text, section: 'head', id, isSelected: no, isUnselected: no, isHead: yes }
file.parallelLines.right.push line file.parallelLines.right.push line
else else
console.log 'unhandled line type...', line console.log 'unhandled line type...', line
else else
file.parallelLines.left.push { lineType: 'context', lineNumber: line.old_line, text: line.text } file.parallelLines.left.push { lineType: 'context', lineNumber: line.old_line, richText: line.rich_text }
file.parallelLines.right.push { lineType: 'context', lineNumber: line.new_line, text: line.text } file.parallelLines.right.push { lineType: 'context', lineNumber: line.new_line, richText: line.rich_text }
if conflict if conflict
file.inlineLines.push { isHeader: yes, id, type: 'new', text: originHeaderText, section: 'origin', isOrigin: yes, isSelected: no, isUnselected: no } file.inlineLines.push { isHeader: yes, id, type: 'new', richText: originHeaderText, section: 'origin', isOrigin: yes, isSelected: no, isUnselected: no }
return data return data
......
window.mergeConflictsData = { window.mergeConflictsData = {
"target_branch": "master", "target_branch": "master",
"source_branch": "mc-ui", "source_branch": "mc-ui",
"commit_sha": "b5fa56eb3f2cea5e21c68b43c7c22b5b96e0e7b3",
"files": [ "files": [
{ {
"commit_sha": "b5fa56eb3f2cea5e21c68b43c7c22b5b96e0e7b3",
"old_path": "lib/component.js", "old_path": "lib/component.js",
"new_path": "lib/component.js", "new_path": "lib/component.js",
"sections": [ "sections": [
......
...@@ -17,7 +17,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -17,7 +17,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs] before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts]
# Allow read any merge_request # Allow read any merge_request
before_action :authorize_read_merge_request! before_action :authorize_read_merge_request!
...@@ -130,10 +130,38 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -130,10 +130,38 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
def conflicts def conflicts
return render_404 unless @merge_request.cannot_be_merged?
respond_to do |format| respond_to do |format|
format.html { render 'show' } format.html { render 'show' }
format.json do
rugged = @merge_request.project.repository.rugged
their_commit = rugged.branches[@merge_request.target_branch].target
our_commit = rugged.lookup(@merge_request.diff_head_sha)
merge = rugged.merge_commits(our_commit, their_commit)
commit_message = "Merge branch '#{@merge_request.target_branch}' into '#{@merge_request.source_branch}'\n\n# Conflicts:"
files = merge.conflicts.map do |conflict|
their_path = conflict[:theirs][:path]
our_path = conflict[:ours][:path]
raise 'path mismatch!' unless their_path == our_path
commit_message << "\n# #{our_path}"
merge_file = merge.merge_file(our_path)
Gitlab::Conflict::File.new(merge_file, conflict, @merge_request.target_branch, @merge_request.source_branch, @merge_request.project.repository)
end
render json: {
target_branch: @merge_request.target_branch,
source_branch: @merge_request.source_branch,
commit_sha: @merge_request.diff_head_sha,
commit_message: commit_message,
files: files
}
end
end end
end end
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
.file-title .file-title
%span {{file.new_path}} %span {{file.new_path}}
.file-actions .file-actions
%a.btn.btn-sm View file @{{file.shortCommitSha}} %a.btn.btn-sm View file @{{conflictsData.shortCommitShab}}
.diff-content.diff-wrap-lines .diff-content.diff-wrap-lines
.diff-wrap-lines.code.file-content.js-syntax-highlight.white .diff-wrap-lines.code.file-content.js-syntax-highlight.white
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
.file-title .file-title
%span {{file.new_path}} %span {{file.new_path}}
.file-actions .file-actions
%a.btn.btn-sm View file @{{file.shortCommitSha}} %a.btn.btn-sm View file @{{conflictsData.shortCommitSha}}
.diff-content.diff-wrap-lines .diff-content.diff-wrap-lines
.diff-wrap-lines.code.file-content.js-syntax-highlight.white .diff-wrap-lines.code.file-content.js-syntax-highlight.white
...@@ -75,13 +75,13 @@ ...@@ -75,13 +75,13 @@
%td.diff-line-num.new_line %td.diff-line-num.new_line
%a {{line.new_line}} %a {{line.new_line}}
%td.line_content %td.line_content
{{{line.text}}} {{{line.rich_text}}}
%template{"v-if" => "line.isHeader"} %template{"v-if" => "line.isHeader"}
%td.diff-line-num.header %td.diff-line-num.header
%td.diff-line-num.header %td.diff-line-num.header
%td.line_content.header %td.line_content.header
%strong {{{line.text}}} %strong {{{line.richText}}}
%button.btn{"@click" => "handleSelected(line.id, line.section)"} Use this %button.btn{"@click" => "handleSelected(line.id, line.section)"} Use this
.content-block.oneline-block.files-changed .content-block.oneline-block.files-changed
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
.commit-message-container.form-group .commit-message-container.form-group
.max-width-marker .max-width-marker
%textarea.form-control.js-commit-message{"placeholder" => 'Your commit message', ":disabled" => "!allResolved"} %textarea.form-control.js-commit-message{":disabled" => "!allResolved"} {{{conflictsData.commitMessage}}}
%button{type: 'button', class: 'btn btn-success js-submit-button', ":disabled" => "!allResolved"} %button{type: 'button', class: 'btn btn-success js-submit-button', ":disabled" => "!allResolved"}
Commit conflict resolution Commit conflict resolution
......
module Gitlab
module Conflict
class File
CONTEXT_LINES = 3
attr_reader :merge_file, :their_path, :their_ref, :our_path, :our_ref, :repository
def initialize(merge_file, conflict, their_ref, our_ref, repository)
@merge_file = merge_file
@their_path = conflict[:theirs][:path]
@our_path = conflict[:ours][:path]
@their_ref = their_ref
@our_ref = our_ref
@repository = repository
end
# Array of Gitlab::Diff::Line objects
def lines
@lines ||= Gitlab::Conflict::Parser.new.parse(merge_file[:data], their_path, our_path)
end
def highlighted_lines
return @highlighted_lines if @highlighted_lines
their_highlight = Gitlab::Highlight.highlight_lines(repository, their_ref, their_path)
our_highlight = Gitlab::Highlight.highlight_lines(repository, our_ref, our_path)
@highlighted_lines = lines.map do |line|
line = line.dup
if line.type == 'old'
line.rich_text = their_highlight[line.old_line - 1].delete("\n")
else
line.rich_text = our_highlight[line.new_line - 1].delete("\n")
end
line
end
end
def sections
return @sections if @sections
chunked_lines = highlighted_lines.chunk { |line| line.type.nil? }
match_line = nil
@sections = chunked_lines.flat_map.with_index do |(no_conflict, lines), i|
section = nil
if no_conflict
conflict_before = i > 0
conflict_after = chunked_lines.peek
if conflict_before && conflict_after
if lines.length > CONTEXT_LINES * 2
tail_lines = lines.last(CONTEXT_LINES)
first_tail_line = tail_lines.first
match_line = Gitlab::Diff::Line.new('',
'match',
first_tail_line.index,
first_tail_line.old_pos,
first_tail_line.new_pos)
section = [
{ conflict: false, lines: lines.first(CONTEXT_LINES) },
{ conflict: false, lines: tail_lines.unshift(match_line) }
]
end
elsif conflict_after
lines = lines.last(CONTEXT_LINES)
elsif conflict_before
lines = lines.first(CONTEXT_LINES)
end
end
if match_line && !section
match_line.text = "@@ -#{match_line.old_pos},#{lines.last.old_pos} +#{match_line.new_pos},#{lines.last.new_pos} @@"
end
section || { conflict: !no_conflict, lines: lines }
end
end
def as_json(opts = nil)
{
old_path: their_path,
new_path: our_path,
sections: sections
}
end
end
end
end
module Gitlab
module Conflict
class Parser
class UnexpectedDelimiter < StandardError
end
class MissingEndDelimiter < StandardError
end
def parse(text, their_path, our_path)
return [] if text.blank?
line_obj_index = 0
line_old = 1
line_new = 1
type = nil
lines = []
conflict_start = "<<<<<<< #{our_path}"
conflict_middle = '======='
conflict_end = ">>>>>>> #{their_path}"
text.each_line.map do |line|
full_line = line.delete("\n")
if full_line == conflict_start
raise UnexpectedDelimiter unless type.nil?
type = 'new'
elsif full_line == conflict_middle
raise UnexpectedDelimiter unless type == 'new'
type = 'old'
elsif full_line == conflict_end
raise UnexpectedDelimiter unless type == 'old'
type = nil
elsif line[0] == '\\'
type = 'nonewline'
lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
else
lines << Gitlab::Diff::Line.new(full_line, type, line_obj_index, line_old, line_new)
line_old += 1 if type != 'new'
line_new += 1 if type != 'old'
line_obj_index += 1
end
end
raise MissingEndDelimiter unless type == nil
lines
end
def empty?
@lines.empty?
end
end
end
end
...@@ -3,6 +3,7 @@ module Gitlab ...@@ -3,6 +3,7 @@ module Gitlab
class Line class Line
attr_reader :type, :index, :old_pos, :new_pos attr_reader :type, :index, :old_pos, :new_pos
attr_accessor :text attr_accessor :text
attr_accessor :rich_text
def initialize(text, type, index, old_pos, new_pos) def initialize(text, type, index, old_pos, new_pos)
@text, @type, @index = text, type, index @text, @type, @index = text, type, index
...@@ -46,6 +47,16 @@ module Gitlab ...@@ -46,6 +47,16 @@ module Gitlab
def meta? def meta?
type == 'match' || type == 'nonewline' type == 'match' || type == 'nonewline'
end end
def as_json(opts = nil)
{
type: type,
old_line: old_line,
new_line: new_line,
text: text,
rich_text: rich_text || text
}
end
end end
end end
end end
require 'spec_helper'
describe Gitlab::Conflict::File, lib: true do
let(:project) { create(:project) }
let(:repository) { project.repository }
let(:rugged) { repository.rugged }
let(:their_ref) { their_commit.oid }
let(:their_commit) { rugged.branches['conflict-a'].target }
let(:our_ref) { our_commit.oid }
let(:our_commit) { rugged.branches['conflict-b'].target }
let(:index) { rugged.merge_commits(our_commit, their_commit) }
let(:conflict) { index.conflicts.last }
let(:merge_file) { index.merge_file('files/ruby/regex.rb') }
let(:conflict_file) { Gitlab::Conflict::File.new(merge_file, conflict, their_ref, our_ref, repository) }
describe '#highlighted_lines' do
def html_to_text(html)
CGI.unescapeHTML(ActionView::Base.full_sanitizer.sanitize(html))
end
it 'returns lines with rich_text' do
conflict_file.highlighted_lines.each do |line|
expect(line).to have_attributes(rich_text: an_instance_of(String))
end
end
it 'returns lines with rich_text matching the text content of the line' do
conflict_file.highlighted_lines.each do |line|
expect(line.text).to eq(html_to_text(line.rich_text))
end
end
end
describe '#sections' do
it 'returns match lines when there is a gap between sections' do
section = conflict_file.sections[5]
match_line = section[:lines][0]
expect(section[:conflict]).to be_falsey
expect(match_line.type).to eq('match')
expect(match_line.text).to eq('@@ -46,53 +46,53 @@')
end
it 'does not return match lines when there is no gap between sections' do
conflict_file.sections.each_with_index do |section, i|
unless i == 5
expect(section[:lines][0].type).not_to eq(5)
end
end
end
it 'sets conflict to false for sections with only unchanged lines' do
conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
without_match = section[:lines].reject { |line| line.type == 'match' }
expect(without_match).to all(have_attributes(type: nil))
end
end
it 'only includes a maximum of CONTEXT_LINES (plus an optional match line) in context sections' do
conflict_file.sections.reject { |section| section[:conflict] }.each do |section|
without_match = section[:lines].reject { |line| line.type == 'match' }
expect(without_match.length).to be <= Gitlab::Conflict::File::CONTEXT_LINES * 2
end
end
it 'sets conflict to true for sections with only changed lines' do
conflict_file.sections.select { |section| section[:conflict] }.each do |section|
section[:lines].each do |line|
expect(line.type).to be_in(['new', 'old'])
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Conflict::Parser, lib: true do
let(:parser) { Gitlab::Conflict::Parser.new }
describe '#parse' do
context 'when the file has valid conflicts' do
let(:text) do
<<CONFLICT
module Gitlab
module Regexp
extend self
def username_regexp
default_regexp
end
<<<<<<< files/ruby/regex.rb
def project_name_regexp
/\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z/
end
def name_regexp
/\A[a-zA-Z0-9_\-\. ]*\z/
=======
def project_name_regex
%r{\A[a-zA-Z0-9][a-zA-Z0-9_\-\. ]*\z}
end
def name_regex
%r{\A[a-zA-Z0-9_\-\. ]*\z}
>>>>>>> files/ruby/regex.rb
end
def path_regexp
default_regexp
end
<<<<<<< files/ruby/regex.rb
def archive_formats_regexp
/(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
=======
def archive_formats_regex
%r{(zip|tar|7z|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)}
>>>>>>> files/ruby/regex.rb
end
def git_reference_regexp
# Valid git ref regexp, see:
# https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
%r{
(?!
(?# doesn't begins with)
\/| (?# rule #6)
(?# doesn't contain)
.*(?:
[\/.]\.| (?# rule #1,3)
\/\/| (?# rule #6)
@\{| (?# rule #8)
\\ (?# rule #9)
)
)
[^\000-\040\177~^:?*\[]+ (?# rule #4-5)
(?# doesn't end with)
(?<!\.lock) (?# rule #1)
(?<![\/.]) (?# rule #6-7)
}x
end
protected
<<<<<<< files/ruby/regex.rb
def default_regexp
/\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
=======
def default_regex
%r{\A[.?]?[a-zA-Z0-9][a-zA-Z0-9_\-\.]*(?<!\.git)\z}
>>>>>>> files/ruby/regex.rb
end
end
end
CONFLICT
end
let(:lines) { parser.parse(text, 'files/ruby/regex.rb', 'files/ruby/regex.rb') }
it 'sets our lines as new lines' do
expect(lines[8..13]).to all(have_attributes(type: 'new'))
expect(lines[26..27]).to all(have_attributes(type: 'new'))
expect(lines[56..57]).to all(have_attributes(type: 'new'))
end
it 'sets their lines as old lines' do
expect(lines[14..19]).to all(have_attributes(type: 'old'))
expect(lines[28..29]).to all(have_attributes(type: 'old'))
expect(lines[58..59]).to all(have_attributes(type: 'old'))
end
it 'sets non-conflicted lines as both' do
expect(lines[0..7]).to all(have_attributes(type: nil))
expect(lines[20..25]).to all(have_attributes(type: nil))
expect(lines[30..55]).to all(have_attributes(type: nil))
expect(lines[60..62]).to all(have_attributes(type: nil))
end
it 'sets consecutive line numbers for index, old_pos, and new_pos' do
old_line_numbers = lines.select { |line| line.type != 'new' }.map(&:old_pos)
new_line_numbers = lines.select { |line| line.type != 'old' }.map(&:new_pos)
expect(lines.map(&:index)).to eq(0.upto(62).to_a)
expect(old_line_numbers).to eq(1.upto(53).to_a)
expect(new_line_numbers).to eq(1.upto(53).to_a)
end
end
context 'when the file contents include conflict delimiters' do
let(:path) { 'README.md' }
def parse_text(text)
parser.parse(text, path, path)
end
it 'raises UnexpectedDelimiter when there is a non-start delimiter first' do
expect { parse_text('=======') }.
to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
expect { parse_text('>>>>>>> README.md') }.
to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
expect { parse_text('>>>>>>> some-other-path.md') }.
not_to raise_error
end
it 'raises UnexpectedDelimiter when a start delimiter is followed by a non-middle delimiter' do
start_text = "<<<<<<< README.md\n"
end_text = "\n=======\n>>>>>>> README.md"
expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
expect { parse_text(start_text + start_text + end_text) }.
to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
not_to raise_error
end
it 'raises UnexpectedDelimiter when a middle delimiter is followed by a non-end delimiter' do
start_text = "<<<<<<< README.md\n=======\n"
end_text = "\n>>>>>>> README.md"
expect { parse_text(start_text + '=======' + end_text) }.
to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
expect { parse_text(start_text + start_text + end_text) }.
to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
not_to raise_error
end
it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
start_text = "<<<<<<< README.md\n=======\n"
expect { parse_text(start_text) }.
to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
expect { parse_text(start_text + '>>>>>>> some-other-path.md') }.
to raise_error(Gitlab::Conflict::Parser::MissingEndDelimiter)
end
end
context 'when lines is blank' do
it { expect(parser.parse('', 'README.md', 'README.md')).to eq([]) }
it { expect(parser.parse(nil, 'README.md', 'README.md')).to eq([]) }
end
end
end
...@@ -19,6 +19,8 @@ module TestEnv ...@@ -19,6 +19,8 @@ module TestEnv
'orphaned-branch' => '45127a9', 'orphaned-branch' => '45127a9',
'binary-encoding' => '7b1cf43', 'binary-encoding' => '7b1cf43',
'gitattributes' => '5a62481', 'gitattributes' => '5a62481',
'conflict-a' => 'dfdd207',
'conflict-b' => '4e75a62',
'expand-collapse-diffs' => '4842455', 'expand-collapse-diffs' => '4842455',
'expand-collapse-files' => '025db92', 'expand-collapse-files' => '025db92',
'expand-collapse-lines' => '238e82d', 'expand-collapse-lines' => '238e82d',
......
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