Commit faa9bd40 authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

Create a Gitlab::Git submodule for conlict-related files

Rename classes to (hopefully) clearer names while we're doing that.
parent 3fcab51e
......@@ -53,7 +53,7 @@ class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::Ap
flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.'
render json: { redirect_to: project_merge_request_url(@project, @merge_request, resolved_conflicts: true) }
rescue Gitlab::Git::Merge::ResolutionError => e
rescue Gitlab::Git::Conflict::Resolver::ResolutionError => e
render status: :bad_request, json: { message: e.message }
end
end
......
......@@ -23,7 +23,7 @@ module MergeRequests
# when there are no conflict files.
conflicts.files.each(&:lines)
@conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
rescue Rugged::OdbError, Gitlab::Git::ConflictParser::UnresolvableError, Gitlab::Git::Merge::ConflictSideMissing
rescue Rugged::OdbError, Gitlab::Git::Conflict::Parser::UnresolvableError, Gitlab::Git::Conflict::Resolver::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end
......
......@@ -186,7 +186,7 @@ module API
lines.each do |line|
next unless line.new_pos == params[:line] && line.type == params[:line_type]
break opts[:line_code] = Gitlab::Git::DiffLineCode.generate(diff.new_path, line.new_pos, line.old_pos)
break opts[:line_code] = Gitlab::Git::Conflict::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
end
break if opts[:line_code]
......
......@@ -173,7 +173,7 @@ module API
lines.each do |line|
next unless line.new_pos == params[:line] && line.type == params[:line_type]
break opts[:line_code] = Gitlab::Git::DiffLineCode.generate(diff.new_path, line.new_pos, line.old_pos)
break opts[:line_code] = Gitlab::Git::Conflict::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
end
break if opts[:line_code]
......
......@@ -23,7 +23,7 @@ module Github
private
def generate_line_code(line)
Gitlab::Git::DiffLineCode.generate(file_path, line.new_pos, line.old_pos)
Gitlab::Git::Conflict::LineCode.generate(file_path, line.new_pos, line.old_pos)
end
def on_diff?
......
......@@ -241,7 +241,7 @@ module Gitlab
end
def generate_line_code(pr_comment)
Gitlab::Git::DiffLineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
Gitlab::Git::Conflict::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
end
def pull_request_comment_attributes(comment)
......
......@@ -6,7 +6,10 @@ module Gitlab
CONTEXT_LINES = 3
attr_reader :merge_request, :raw
attr_reader :merge_request
# 'raw' holds the Gitlab::Git::Conflict::File that this instance wraps
attr_reader :raw
delegate :type, :content, :their_path, :our_path, :our_mode, :our_blob, :repository, to: :raw
......@@ -107,7 +110,7 @@ module Gitlab
end
def line_code(line)
Gitlab::Git::DiffLineCode.generate(our_path, line.new_pos, line.old_pos)
Gitlab::Git::Conflict::LineCode.generate(our_path, line.new_pos, line.old_pos)
end
def create_match_line(line)
......
module Gitlab
module Conflict
class FileCollection
attr_reader :merge_request, :merge
attr_reader :merge_request, :resolver
def initialize(merge_request)
source_repo = merge_request.source_project.repository.raw
our_commit = merge_request.source_branch_head.raw
their_commit = merge_request.target_branch_head.raw
target_repo = merge_request.target_project.repository.raw
@merge = Gitlab::Git::Merge.new(source_repo, our_commit, target_repo, their_commit)
@resolver = Gitlab::Git::Conflict::Resolver.new(source_repo, our_commit, target_repo, their_commit)
@merge_request = merge_request
end
......@@ -18,11 +18,11 @@ module Gitlab
target_branch: merge_request.target_branch,
commit_message: commit_message || default_commit_message
}
merge.resolve_conflicts(user, files, args)
resolver.resolve_conflicts(user, files, args)
end
def files
@files ||= merge.conflicts.map do |conflict_file|
@files ||= resolver.conflicts.map do |conflict_file|
Gitlab::Conflict::File.new(conflict_file, merge_request: merge_request)
end
end
......
......@@ -49,7 +49,7 @@ module Gitlab
def line_code(line)
return if line.meta?
Gitlab::Git::DiffLineCode.generate(file_path, line.new_pos, line.old_pos)
Gitlab::Git::Conflict::LineCode.generate(file_path, line.new_pos, line.old_pos)
end
def line_for_line_code(code)
......
module Gitlab
module Git
module Conflict
class File
attr_reader :content, :their_path, :our_path, :our_mode, :repository
def initialize(repository, commit_oid, conflict, content)
@repository = repository
@commit_oid = commit_oid
@their_path = conflict[:theirs][:path]
@our_path = conflict[:ours][:path]
@our_mode = conflict[:ours][:mode]
@content = content
end
def lines
return @lines if defined?(@lines)
begin
@type = 'text'
@lines = Gitlab::Git::Conflict::Parser.parse(content,
our_path: our_path,
their_path: their_path)
rescue Gitlab::Git::Conflict::Parser::ParserError
@type = 'text-editor'
@lines = nil
end
end
def type
lines unless @type
@type.inquiry
end
def our_blob
# REFACTOR NOTE: the source of `commit_oid` used to be
# `merge_request.diff_refs.head_sha`. Instead of passing this value
# around the new lib structure, I decided to use `@commit_oid` which is
# equivalent to `merge_request.source_branch_head.raw.rugged_commit.oid`.
# That is what `merge_request.diff_refs.head_sha` is equivalent to when
# `merge_request` is not persisted (see `MergeRequest#diff_head_commit`).
# I think using the same oid is more consistent anyways, but if Conflicts
# start breaking, the change described above is a good place to look at.
@our_blob ||= repository.blob_at(@commit_oid, our_path)
end
def line_code(line)
Gitlab::Git::Conflict::LineCode.generate(our_path, line[:line_new], line[:line_old])
end
def resolve_lines(resolution)
section_id = nil
lines.map do |line|
unless line[:type]
section_id = nil
next line
end
section_id ||= line_code(line)
case resolution[section_id]
when 'head'
next unless line[:type] == 'new'
when 'origin'
next unless line[:type] == 'old'
else
raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Missing resolution for section ID: #{section_id}"
end
line
end.compact
end
def resolve_content(resolution)
if resolution == content
raise Gitlab::Git::Conflict::Resolver::ResolutionError, "Resolved content has no changes for file #{our_path}"
end
resolution
end
end
end
end
end
module Gitlab
module Git
module Conflict
class LineCode
def self.generate(file_path, new_line_position, old_line_position)
"#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}"
end
end
end
end
end
module Gitlab
module Git
module Conflict
class Parser
UnresolvableError = Class.new(StandardError)
UnmergeableFile = Class.new(UnresolvableError)
UnsupportedEncoding = Class.new(UnresolvableError)
# Recoverable errors - the conflict can be resolved in an editor, but not with
# sections.
ParserError = Class.new(StandardError)
UnexpectedDelimiter = Class.new(ParserError)
MissingEndDelimiter = Class.new(ParserError)
class << self
def parse(text, our_path:, their_path:, parent_file: nil)
validate_text!(text)
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
validate_delimiter!(type.nil?)
type = 'new'
elsif full_line == conflict_middle
validate_delimiter!(type == 'new')
type = 'old'
elsif full_line == conflict_end
validate_delimiter!(type == 'old')
type = nil
elsif line[0] == '\\'
type = 'nonewline'
lines << {
full_line: full_line,
type: type,
line_obj_index: line_obj_index,
line_old: line_old,
line_new: line_new
}
else
lines << {
full_line: full_line,
type: type,
line_obj_index: line_obj_index,
line_old: line_old,
line_new: 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
private
def validate_text!(text)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 200.kilobytes
text.force_encoding('UTF-8')
raise UnsupportedEncoding unless text.valid_encoding?
end
def validate_delimiter!(condition)
raise UnexpectedDelimiter unless condition
end
end
end
end
end
end
module Gitlab
module Git
module Conflict
class Resolver
ConflictSideMissing = Class.new(StandardError)
ResolutionError = Class.new(StandardError)
def initialize(repository, our_commit, target_repository, their_commit)
@repository = repository
@our_commit = our_commit.rugged_commit
@target_repository = target_repository
@their_commit = their_commit.rugged_commit
end
def conflicts
@conflicts ||= begin
target_index = @target_repository.rugged.merge_commits(@our_commit, @their_commit)
# We don't need to do `with_repo_branch_commit` here, because the target
# project always fetches source refs when creating merge request diffs.
target_index.conflicts.map do |conflict|
raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
Gitlab::Git::Conflict::File.new(
@target_repository,
@our_commit.oid,
conflict,
target_index.merge_file(conflict[:ours][:path])[:data]
)
end
end
end
def resolve_conflicts(user, files, source_branch:, target_branch:, commit_message:)
@repository.with_repo_branch_commit(@target_repository, target_branch) do
files.each do |file_params|
conflict_file = conflict_for_path(file_params[:old_path], file_params[:new_path])
write_resolved_file_to_index(conflict_file, file_params)
end
unless index.conflicts.empty?
missing_files = index.conflicts.map { |file| file[:ours][:path] }
raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}"
end
commit_params = {
message: commit_message,
parents: [@our_commit, @their_commit].map(&:oid)
}
@repository.commit_index(user, source_branch, index, commit_params)
end
end
def conflict_for_path(old_path, new_path)
conflicts.find do |conflict|
conflict.their_path == old_path && conflict.our_path == new_path
end
end
private
# We can only write when getting the merge index from the source
# project, because we will write to that project. We don't use this all
# the time because this fetches a ref into the source project, which
# isn't needed for reading.
def index
@index ||= @repository.rugged.merge_commits(@our_commit, @their_commit)
end
def write_resolved_file_to_index(file, params)
if params[:sections]
resolved_lines = file.resolve_lines(params[:sections])
new_file = resolved_lines.map { |line| line[:full_line] }.join("\n")
new_file << "\n" if file.our_blob.data.ends_with?("\n")
elsif params[:content]
new_file = file.resolve_content(params[:content])
end
our_path = file.our_path
index.add(path: our_path, oid: @repository.rugged.write(new_file, :blob), mode: file.our_mode)
index.conflict_remove(our_path)
end
end
end
end
end
module Gitlab
module Git
class ConflictFile
attr_reader :content, :their_path, :our_path, :our_mode, :repository
def initialize(repository, commit_oid, conflict, content)
@repository = repository
@commit_oid = commit_oid
@their_path = conflict[:theirs][:path]
@our_path = conflict[:ours][:path]
@our_mode = conflict[:ours][:mode]
@content = content
end
def lines
return @lines if defined?(@lines)
begin
@type = 'text'
@lines = Gitlab::Git::ConflictParser.parse(content,
our_path: our_path,
their_path: their_path)
rescue Gitlab::Git::ConflictParser::ParserError
@type = 'text-editor'
@lines = nil
end
end
def type
lines unless @type
@type.inquiry
end
def our_blob
# REFACTOR NOTE: the source of `commit_oid` used to be
# `merge_request.diff_refs.head_sha`. Instead of passing this value
# around the new lib structure, I decided to use `@commit_oid` which is
# equivalent to `merge_request.source_branch_head.raw.rugged_commit.oid`.
# That is what `merge_request.diff_refs.head_sha` is equivalent to when
# `merge_request` is not persisted (see `MergeRequest#diff_head_commit`).
# I think using the same oid is more consistent anyways, but if Conflicts
# start breaking, the change described above is a good place to look at.
@our_blob ||= repository.blob_at(@commit_oid, our_path)
end
def line_code(line)
Gitlab::Git::DiffLineCode.generate(our_path, line[:line_new], line[:line_old])
end
def resolve_lines(resolution)
section_id = nil
lines.map do |line|
unless line[:type]
section_id = nil
next line
end
section_id ||= line_code(line)
case resolution[section_id]
when 'head'
next unless line[:type] == 'new'
when 'origin'
next unless line[:type] == 'old'
else
raise Gitlab::Git::Merge::ResolutionError, "Missing resolution for section ID: #{section_id}"
end
line
end.compact
end
def resolve_content(resolution)
if resolution == content
raise Gitlab::Git::Merge::ResolutionError, "Resolved content has no changes for file #{our_path}"
end
resolution
end
end
end
end
module Gitlab
module Git
class ConflictParser
UnresolvableError = Class.new(StandardError)
UnmergeableFile = Class.new(UnresolvableError)
UnsupportedEncoding = Class.new(UnresolvableError)
# Recoverable errors - the conflict can be resolved in an editor, but not with
# sections.
ParserError = Class.new(StandardError)
UnexpectedDelimiter = Class.new(ParserError)
MissingEndDelimiter = Class.new(ParserError)
class << self
def parse(text, our_path:, their_path:, parent_file: nil)
validate_text!(text)
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
validate_delimiter!(type.nil?)
type = 'new'
elsif full_line == conflict_middle
validate_delimiter!(type == 'new')
type = 'old'
elsif full_line == conflict_end
validate_delimiter!(type == 'old')
type = nil
elsif line[0] == '\\'
type = 'nonewline'
lines << {
full_line: full_line,
type: type,
line_obj_index: line_obj_index,
line_old: line_old,
line_new: line_new
}
else
lines << {
full_line: full_line,
type: type,
line_obj_index: line_obj_index,
line_old: line_old,
line_new: 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
private
def validate_text!(text)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 200.kilobytes
text.force_encoding('UTF-8')
raise UnsupportedEncoding unless text.valid_encoding?
end
def validate_delimiter!(condition)
raise UnexpectedDelimiter unless condition
end
end
end
end
end
module Gitlab
module Git
class DiffLineCode
def self.generate(file_path, new_line_position, old_line_position)
"#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}"
end
end
end
end
module Gitlab
module Git
class Merge
ConflictSideMissing = Class.new(StandardError)
ResolutionError = Class.new(StandardError)
def initialize(repository, our_commit, target_repository, their_commit)
@repository = repository
@our_commit = our_commit.rugged_commit
@target_repository = target_repository
@their_commit = their_commit.rugged_commit
end
def conflicts
@conflicts ||= begin
target_index = @target_repository.rugged.merge_commits(@our_commit, @their_commit)
# We don't need to do `with_repo_branch_commit` here, because the target
# project always fetches source refs when creating merge request diffs.
target_index.conflicts.map do |conflict|
raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours]
Gitlab::Git::ConflictFile.new(
@target_repository,
@our_commit.oid,
conflict,
target_index.merge_file(conflict[:ours][:path])[:data]
)
end
end
end
def resolve_conflicts(user, files, source_branch:, target_branch:, commit_message:)
@repository.with_repo_branch_commit(@target_repository, target_branch) do
files.each do |file_params|
conflict_file = conflict_for_path(file_params[:old_path], file_params[:new_path])
write_resolved_file_to_index(conflict_file, file_params)
end
unless index.conflicts.empty?
missing_files = index.conflicts.map { |file| file[:ours][:path] }
raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}"
end
commit_params = {
message: commit_message,
parents: [@our_commit, @their_commit].map(&:oid)
}
@repository.commit_index(user, source_branch, index, commit_params)
end
end
def conflict_for_path(old_path, new_path)
conflicts.find do |conflict|
conflict.their_path == old_path && conflict.our_path == new_path
end
end
private
# We can only write when getting the merge index from the source
# project, because we will write to that project. We don't use this all
# the time because this fetches a ref into the source project, which
# isn't needed for reading.
def index
@index ||= @repository.rugged.merge_commits(@our_commit, @their_commit)
end
def write_resolved_file_to_index(file, params)
if params[:sections]
resolved_lines = file.resolve_lines(params[:sections])
new_file = resolved_lines.map { |line| line[:full_line] }.join("\n")
new_file << "\n" if file.our_blob.data.ends_with?("\n")
elsif params[:content]
new_file = file.resolve_content(params[:content])
end
our_path = file.our_path
index.add(path: our_path, oid: @repository.rugged.write(new_file, :blob), mode: file.our_mode)
index.conflict_remove(our_path)
end
end
end
end
......@@ -38,7 +38,7 @@ module Gitlab
end
def generate_line_code(line)
Gitlab::Git::DiffLineCode.generate(file_path, line.new_pos, line.old_pos)
Gitlab::Git::Conflict::LineCode.generate(file_path, line.new_pos, line.old_pos)
end
def on_diff?
......
......@@ -17,8 +17,8 @@ describe Projects::MergeRequests::ConflictsController do
describe 'GET show' do
context 'when the conflicts cannot be resolved in the UI' do
before do
allow(Gitlab::Git::ConflictParser).to receive(:parse)
.and_raise(Gitlab::Git::ConflictParser::UnmergeableFile)
allow(Gitlab::Git::Conflict::Parser).to receive(:parse)
.and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile)
get :show,
namespace_id: merge_request_with_conflicts.project.namespace.to_param,
......@@ -109,8 +109,8 @@ describe Projects::MergeRequests::ConflictsController do
context 'when the conflicts cannot be resolved in the UI' do
before do
allow(Gitlab::Git::ConflictParser).to receive(:parse)
.and_raise(Gitlab::Git::ConflictParser::UnmergeableFile)
allow(Gitlab::Git::Conflict::Parser).to receive(:parse)
.and_raise(Gitlab::Git::Conflict::Parser::UnmergeableFile)
conflict_for_path('files/ruby/regex.rb')
end
......