From 5c5259335f8bcc4de117c1e36648a269911281fb Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 1 Sep 2016 11:39:37 +0100 Subject: Add instrumentation to conflict classes --- config/initializers/metrics.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb index be22085b0df..3b8771543e4 100644 --- a/config/initializers/metrics.rb +++ b/config/initializers/metrics.rb @@ -67,6 +67,7 @@ if Gitlab::Metrics.enabled? ['app', 'finders'] => ['app', 'finders'], ['app', 'mailers', 'emails'] => ['app', 'mailers'], ['app', 'services', '**'] => ['app', 'services'], + ['lib', 'gitlab', 'conflicts'] => ['lib'], ['lib', 'gitlab', 'diff'] => ['lib'], ['lib', 'gitlab', 'email', 'message'] => ['lib'], ['lib', 'gitlab', 'checks'] => ['lib'] -- cgit v1.2.1 From 3f71c43e88c56bb5310c8814cd9f95cafb4f53ef Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 1 Sep 2016 13:59:10 +0100 Subject: Allow setting content for resolutions When reading conflicts: 1. Add a `type` field. `text` works as before, and has `sections`; `text-editor` is a file with ambiguous conflict markers that can only be resolved in an editor. 2. Add a `content_path` field pointing to a JSON representation of the file's content for a single file. 3. Hitting `content_path` returns a similar datastructure to the `file`, but without the `content_path` and `sections` fields, and with a `content` field containing the full contents of the file (with conflict markers). When writing conflicts: 1. Instead of `sections` being at the top level, they are now in a `files` array. This matches the read format better. 2. The `files` array contains file hashes, each of which must contain: a. `new_path` b. `old_path` c. EITHER `sections` (which works as before) or `content` (with the full content of the resolved file). --- app/controllers/application_controller.rb | 7 +- .../projects/merge_requests_controller.rb | 20 ++- app/models/merge_request.rb | 2 +- app/services/merge_requests/resolve_service.rb | 24 ++- config/routes/project.rb | 1 + lib/gitlab/conflict/file.rb | 51 ++++++- lib/gitlab/conflict/file_collection.rb | 4 + lib/gitlab/conflict/parser.rb | 15 +- lib/gitlab/conflict/resolution_error.rb | 6 + .../projects/merge_requests_controller_spec.rb | 168 +++++++++++++++++++-- .../merge_requests/resolve_service_spec.rb | 139 +++++++++++++++-- 11 files changed, 395 insertions(+), 42 deletions(-) create mode 100644 lib/gitlab/conflict/resolution_error.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b3455e04c29..bf37421771f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -114,7 +114,12 @@ class ApplicationController < ActionController::Base end def render_404 - render file: Rails.root.join("public", "404"), layout: false, status: "404" + respond_to do |format| + format.json { head :not_found } + format.any do + render file: Rails.root.join("public", "404"), layout: false, status: "404" + end + end end def no_cache_headers diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 869d96b86f4..2c7a0062f2b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -9,15 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :conflicts, :builds, :pipelines, :merge, :merge_check, + :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check, :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] - before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :builds, :pipelines] + before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_commit_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs] - before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines] + before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :apply_diff_view_cookie!, only: [:new_diffs] before_action :build_merge_request, only: [:new, :new_diffs] @@ -33,7 +33,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :authenticate_user!, only: [:assign_related_issues] - before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :resolve_conflicts] + before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts] def index @merge_requests = merge_requests_collection @@ -170,6 +170,16 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def conflict_for_path + return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui? + + file = @merge_request.conflicts.file_for_path(params[:old_path], params[:new_path]) + + return render_404 unless file + + render json: file, full_content: true + end + def resolve_conflicts return render_404 unless @merge_request.conflicts_can_be_resolved_in_ui? @@ -184,7 +194,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController flash[:notice] = 'All merge conflicts were resolved. The merge request can now be merged.' render json: { redirect_to: namespace_project_merge_request_url(@project.namespace, @project, @merge_request, resolved_conflicts: true) } - rescue Gitlab::Conflict::File::MissingResolution => e + rescue Gitlab::Conflict::ResolutionError => e render status: :bad_request, json: { message: e.message } end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a743bf313ae..1fb0371fe06 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -868,7 +868,7 @@ class MergeRequest < ActiveRecord::Base # files. conflicts.files.each(&:lines) @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0 - rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing + rescue Rugged::OdbError, Gitlab::Conflict::Parser::UnresolvableError, Gitlab::Conflict::FileCollection::ConflictSideMissing @conflicts_can_be_resolved_in_ui = false end end diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb index 19caa038c44..d22a1d3e0ad 100644 --- a/app/services/merge_requests/resolve_service.rb +++ b/app/services/merge_requests/resolve_service.rb @@ -1,5 +1,8 @@ module MergeRequests class ResolveService < MergeRequests::BaseService + class MissingFiles < Gitlab::Conflict::ResolutionError + end + attr_accessor :conflicts, :rugged, :merge_index, :merge_request def execute(merge_request) @@ -10,8 +13,16 @@ module MergeRequests fetch_their_commit! - conflicts.files.each do |file| - write_resolved_file_to_index(file, params[:sections]) + params[:files].each do |file_params| + conflict_file = merge_request.conflicts.file_for_path(file_params[:old_path], file_params[:new_path]) + + write_resolved_file_to_index(conflict_file, file_params) + end + + unless merge_index.conflicts.empty? + missing_files = merge_index.conflicts.map { |file| file[:ours][:path] } + + raise MissingFiles, "Missing resolutions for the following files: #{missing_files.join(', ')}" end commit_params = { @@ -23,8 +34,13 @@ module MergeRequests project.repository.resolve_conflicts(current_user, merge_request.source_branch, commit_params) end - def write_resolved_file_to_index(file, resolutions) - new_file = file.resolve_lines(resolutions).map(&:text).join("\n") + def write_resolved_file_to_index(file, params) + new_file = if params[:sections] + file.resolve_lines(params[:sections]).map(&:text).join("\n") + elsif params[:content] + file.resolve_content(params[:content]) + end + our_path = file.our_path merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode) diff --git a/config/routes/project.rb b/config/routes/project.rb index f9d58f5d5b2..cbce9cd47a0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -267,6 +267,7 @@ resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: get :commits get :diffs get :conflicts + get :conflict_for_path get :builds get :pipelines get :merge_check diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb index dff9e29c6a5..26a9f170298 100644 --- a/lib/gitlab/conflict/file.rb +++ b/lib/gitlab/conflict/file.rb @@ -4,12 +4,12 @@ module Gitlab include Gitlab::Routing.url_helpers include IconsHelper - class MissingResolution < StandardError + class MissingResolution < ResolutionError end CONTEXT_LINES = 3 - attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository + attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository, :type def initialize(merge_file_result, conflict, merge_request:) @merge_file_result = merge_file_result @@ -21,12 +21,24 @@ module Gitlab @match_line_headers = {} end + def content + merge_file_result[:data] + end + # Array of Gitlab::Diff::Line objects def lines - @lines ||= Gitlab::Conflict::Parser.new.parse(merge_file_result[:data], + return @lines if defined?(@lines) + + begin + @type = 'text' + @lines = Gitlab::Conflict::Parser.new.parse(content, our_path: our_path, their_path: their_path, parent_file: self) + rescue Gitlab::Conflict::Parser::ParserError + @type = 'text-editor' + @lines = nil + end end def resolve_lines(resolution) @@ -53,6 +65,14 @@ module Gitlab end.compact end + def resolve_content(resolution) + if resolution == content + raise MissingResolution, "Resolved content has no changes for file #{our_path}" + end + + resolution + end + def highlight_lines! their_file = lines.reject { |line| line.type == 'new' }.map(&:text).join("\n") our_file = lines.reject { |line| line.type == 'old' }.map(&:text).join("\n") @@ -170,21 +190,36 @@ module Gitlab match_line.text = "@@ -#{match_line.old_pos},#{line.old_pos} +#{match_line.new_pos},#{line.new_pos} @@#{header}" end - def as_json(opts = nil) - { + def as_json(opts = {}) + json_hash = { old_path: their_path, new_path: our_path, blob_icon: file_type_icon_class('file', our_mode, our_path), blob_path: namespace_project_blob_path(merge_request.project.namespace, merge_request.project, - ::File.join(merge_request.diff_refs.head_sha, our_path)), - sections: sections + ::File.join(merge_request.diff_refs.head_sha, our_path)) } + + if opts[:full_content] + json_hash.merge(content: content) + else + json_hash.merge!(sections: sections) if type == 'text' + + json_hash.merge(type: type, content_path: content_path) + end + end + + def content_path + conflict_for_path_namespace_project_merge_request_path(merge_request.project.namespace, + merge_request.project, + merge_request, + old_path: their_path, + new_path: our_path) end # Don't try to print merge_request or repository. def inspect - instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode].map do |instance_variable| + instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode, :type].map do |instance_variable| value = instance_variable_get("@#{instance_variable}") "#{instance_variable}=\"#{value}\"" diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb index bbd0427a2c8..fa5bd4649d4 100644 --- a/lib/gitlab/conflict/file_collection.rb +++ b/lib/gitlab/conflict/file_collection.rb @@ -30,6 +30,10 @@ module Gitlab end end + def file_for_path(old_path, new_path) + files.find { |file| file.their_path == old_path && file.our_path == new_path } + end + def as_json(opts = nil) { target_branch: merge_request.target_branch, diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb index 98e842cded3..ddd657903fb 100644 --- a/lib/gitlab/conflict/parser.rb +++ b/lib/gitlab/conflict/parser.rb @@ -1,19 +1,24 @@ module Gitlab module Conflict class Parser - class ParserError < StandardError + class UnresolvableError < StandardError end - class UnexpectedDelimiter < ParserError + class UnmergeableFile < UnresolvableError end - class MissingEndDelimiter < ParserError + class UnsupportedEncoding < UnresolvableError + end + + # Recoverable errors - the conflict can be resolved in an editor, but not with + # sections. + class ParserError < StandardError end - class UnmergeableFile < ParserError + class UnexpectedDelimiter < ParserError end - class UnsupportedEncoding < ParserError + class MissingEndDelimiter < ParserError end def parse(text, our_path:, their_path:, parent_file: nil) diff --git a/lib/gitlab/conflict/resolution_error.rb b/lib/gitlab/conflict/resolution_error.rb new file mode 100644 index 00000000000..a0f2006bc24 --- /dev/null +++ b/lib/gitlab/conflict/resolution_error.rb @@ -0,0 +1,6 @@ +module Gitlab + module Conflict + class ResolutionError < StandardError + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 84298f8bef4..1311b4aa264 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -570,7 +570,7 @@ describe Projects::MergeRequestsController do context 'when the conflicts cannot be resolved in the UI' do before do allow_any_instance_of(Gitlab::Conflict::Parser). - to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnexpectedDelimiter) + to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) get :conflicts, namespace_id: merge_request_with_conflicts.project.namespace.to_param, @@ -655,6 +655,61 @@ describe Projects::MergeRequestsController do id: merge_request.iid expect(merge_request.reload.title).to eq(merge_request.wipless_title) + end + + describe 'GET conflict_for_path' do + let(:json_response) { JSON.parse(response.body) } + + def conflict_for_path(path) + get :conflict_for_path, + namespace_id: merge_request_with_conflicts.project.namespace.to_param, + project_id: merge_request_with_conflicts.project.to_param, + id: merge_request_with_conflicts.iid, + old_path: path, + new_path: path, + format: 'json' + end + + context 'when the conflicts cannot be resolved in the UI' do + before do + allow_any_instance_of(Gitlab::Conflict::Parser). + to receive(:parse).and_raise(Gitlab::Conflict::Parser::UnmergeableFile) + + conflict_for_path('files/ruby/regex.rb') + end + + + it 'returns a 404 status code' do + expect(response).to have_http_status(:not_found) + end + end + + context 'when the file does not exist cannot be resolved in the UI' do + before { conflict_for_path('files/ruby/regexp.rb') } + + it 'returns a 404 status code' do + expect(response).to have_http_status(:not_found) + end + end + + context 'with an existing file' do + let(:path) { 'files/ruby/regex.rb' } + + before { conflict_for_path(path) } + + it 'returns a 200 status code' do + expect(response).to have_http_status(:ok) + end + + it 'returns the file in JSON format' do + content = merge_request_with_conflicts.conflicts.file_for_path(path, path).content + + expect(json_response).to include('old_path' => path, + 'new_path' => path, + 'blob_icon' => 'file-text-o', + 'blob_path' => a_string_ending_with(path), + 'content' => content) + end end end @@ -662,22 +717,37 @@ describe Projects::MergeRequestsController do let(:json_response) { JSON.parse(response.body) } let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha } - def resolve_conflicts(sections) + def resolve_conflicts(files) post :resolve_conflicts, namespace_id: merge_request_with_conflicts.project.namespace.to_param, project_id: merge_request_with_conflicts.project.to_param, id: merge_request_with_conflicts.iid, format: 'json', - sections: sections, + files: files, commit_message: 'Commit message' end context 'with valid params' do before do - resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin') + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'sections' => { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) end it 'creates a new commit on the branch' do @@ -692,7 +762,23 @@ describe Projects::MergeRequestsController do context 'when sections are missing' do before do - resolve_conflicts('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head') + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'sections' => { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' + } + } + ] + + resolve_conflicts(resolved_files) end it 'returns a 400 error' do @@ -700,7 +786,71 @@ describe Projects::MergeRequestsController do end it 'has a message with the name of the first missing section' do - expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9') + expect(json_response['message']).to include('6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + + context 'when files are missing' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the name of the missing file' do + expect(json_response['message']).to include('files/ruby/popen.rb') + end + + it 'does not create a new commit' do + expect(original_head_sha).to eq(merge_request_with_conflicts.source_branch_head.sha) + end + end + + context 'when a file has identical content to the conflict' do + before do + resolved_files = [ + { + 'new_path' => 'files/ruby/popen.rb', + 'old_path' => 'files/ruby/popen.rb', + 'content' => merge_request_with_conflicts.conflicts.file_for_path('files/ruby/popen.rb', 'files/ruby/popen.rb').content + }, { + 'new_path' => 'files/ruby/regex.rb', + 'old_path' => 'files/ruby/regex.rb', + 'sections' => { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ] + + resolve_conflicts(resolved_files) + end + + it 'returns a 400 error' do + expect(response).to have_http_status(:bad_request) + end + + it 'has a message with the path of the problem file' do + expect(json_response['message']).to include('files/ruby/popen.rb') end it 'does not create a new commit' do diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index d71932458fa..e667e93bea4 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -24,15 +24,26 @@ describe MergeRequests::ResolveService do end describe '#execute' do - context 'with valid params' do + context 'with section params' do let(:params) do { - sections: { - '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', - '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' - }, + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + sections: { + '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' + } + }, { + old_path: 'files/ruby/regex.rb', + new_path: 'files/ruby/regex.rb', + sections: { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ], commit_message: 'This is a commit message!' } end @@ -74,8 +85,96 @@ describe MergeRequests::ResolveService do end end - context 'when a resolution is missing' do - let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } } + context 'with content and sections params' do + let(:popen_content) { "class Popen\nend" } + + let(:params) do + { + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + content: popen_content + }, { + old_path: 'files/ruby/regex.rb', + new_path: 'files/ruby/regex.rb', + sections: { + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin', + '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin' + } + } + ], + commit_message: 'This is a commit message!' + } + end + + before do + MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + end + + it 'creates a commit with the message' do + expect(merge_request.source_branch_head.message).to eq(params[:commit_message]) + end + + it 'creates a commit with the correct parents' do + expect(merge_request.source_branch_head.parents.map(&:id)). + to eq(['1450cd639e0bc6721eb02800169e464f212cde06', + '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) + end + + it 'sets the content to the content given' do + blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha, + 'files/ruby/popen.rb') + + expect(blob.data).to eq(popen_content) + end + end + + context 'when a resolution section is missing' do + let(:invalid_params) do + { + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + content: '' + }, { + old_path: 'files/ruby/regex.rb', + new_path: 'files/ruby/regex.rb', + sections: { '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head' } + } + ], + commit_message: 'This is a commit message!' + } + end + + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + + it 'raises a MissingResolution error' do + expect { service.execute(merge_request) }. + to raise_error(Gitlab::Conflict::File::MissingResolution) + end + end + + context 'when the content of a file is unchanged' do + let(:invalid_params) do + { + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + content: '' + }, { + old_path: 'files/ruby/regex.rb', + new_path: 'files/ruby/regex.rb', + content: merge_request.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content + } + ], + commit_message: 'This is a commit message!' + } + end + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } it 'raises a MissingResolution error' do @@ -83,5 +182,27 @@ describe MergeRequests::ResolveService do to raise_error(Gitlab::Conflict::File::MissingResolution) end end + + context 'when a file is missing' do + let(:invalid_params) do + { + files: [ + { + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', + content: '' + } + ], + commit_message: 'This is a commit message!' + } + end + + let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } + + it 'raises a MissingFiles error' do + expect { service.execute(merge_request) }. + to raise_error(MergeRequests::ResolveService::MissingFiles) + end + end end end -- cgit v1.2.1 From 9727366b5a0a39a125925e2a7a78ed47e29b02f7 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 1 Sep 2016 16:19:16 +0100 Subject: Make RuboCop happy --- spec/controllers/projects/merge_requests_controller_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 1311b4aa264..b4f637c93e2 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -678,7 +678,6 @@ describe Projects::MergeRequestsController do conflict_for_path('files/ruby/regex.rb') end - it 'returns a 404 status code' do expect(response).to have_http_status(:not_found) end -- cgit v1.2.1 From 241cca011f9a35c03e8f5fae1381a4af8a8f26bb Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 1 Sep 2016 16:20:29 +0100 Subject: Fix specs --- app/controllers/application_controller.rb | 4 ++-- spec/support/test_env.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bf37421771f..81c0fa26d18 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -115,10 +115,10 @@ class ApplicationController < ActionController::Base def render_404 respond_to do |format| - format.json { head :not_found } - format.any do + format.html do render file: Rails.root.join("public", "404"), layout: false, status: "404" end + format.any { head :not_found } end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index d56274d0979..243d671c521 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -26,10 +26,10 @@ module TestEnv 'expand-collapse-lines' => '238e82d', 'video' => '8879059', 'crlf-diff' => '5938907', - 'conflict-start' => '75284c7', + 'conflict-start' => '824be60', 'conflict-resolvable' => '1450cd6', 'conflict-binary-file' => '259a6fb', - 'conflict-contains-conflict-markers' => '5e0964c', + 'conflict-contains-conflict-markers' => '78a3086', 'conflict-missing-side' => 'eb227b3', 'conflict-non-utf8' => 'd0a293c', 'conflict-too-large' => '39fa04f', -- cgit v1.2.1 From 082be0917cf15d410b1db3ca49b32049a56c117e Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 1 Sep 2016 16:50:53 +0100 Subject: Fix MR model spec --- spec/models/merge_request_spec.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 5884b4cff8c..91a423b670c 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1155,12 +1155,6 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey end - it 'returns a falsey value when the conflicts contain a file with ambiguous conflict markers' do - merge_request = create_merge_request('conflict-contains-conflict-markers') - - expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey - end - it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do merge_request = create_merge_request('conflict-missing-side') @@ -1172,6 +1166,12 @@ describe MergeRequest, models: true do expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy end + + it 'returns a truthy value when the conflicts have to be resolved in an editor' do + merge_request = create_merge_request('conflict-contains-conflict-markers') + + expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy + end end describe "#forked_source_project_missing?" do -- cgit v1.2.1 From 98dd958df3b4dd1b58b3a3387ea9d52dc78af46b Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 1 Sep 2016 17:44:35 +0100 Subject: Fix resolve service specs --- spec/services/merge_requests/resolve_service_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index e667e93bea4..388abb6a0df 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -60,7 +60,7 @@ describe MergeRequests::ResolveService do it 'creates a commit with the correct parents' do expect(merge_request.source_branch_head.parents.map(&:id)). to eq(['1450cd639e0bc6721eb02800169e464f212cde06', - '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) + '824be604a34828eb682305f0d963056cfac87b2d']) end end @@ -120,7 +120,7 @@ describe MergeRequests::ResolveService do it 'creates a commit with the correct parents' do expect(merge_request.source_branch_head.parents.map(&:id)). to eq(['1450cd639e0bc6721eb02800169e464f212cde06', - '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b']) + '824be604a34828eb682305f0d963056cfac87b2d']) end it 'sets the content to the content given' do -- cgit v1.2.1 From 7529bbae944823041e8690c011c4cfb39534074b Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 2 Sep 2016 16:16:54 +0100 Subject: Add JSON Schema --- .../projects/merge_requests_controller_spec.rb | 4 + spec/fixtures/api/schemas/conflicts.json | 137 +++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 spec/fixtures/api/schemas/conflicts.json diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index b4f637c93e2..06b37aa4997 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -597,6 +597,10 @@ describe Projects::MergeRequestsController do format: 'json' end + it 'matches the schema' do + expect(response).to match_response_schema('conflicts') + end + it 'includes meta info about the MR' do expect(json_response['commit_message']).to include('Merge branch') expect(json_response['commit_sha']).to match(/\h{40}/) diff --git a/spec/fixtures/api/schemas/conflicts.json b/spec/fixtures/api/schemas/conflicts.json new file mode 100644 index 00000000000..a947783d505 --- /dev/null +++ b/spec/fixtures/api/schemas/conflicts.json @@ -0,0 +1,137 @@ +{ + "type": "object", + "required": [ + "commit_message", + "commit_sha", + "source_branch", + "target_branch", + "files" + ], + "properties": { + "commit_message": {"type": "string"}, + "commit_sha": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "source_branch": {"type": "string"}, + "target_branch": {"type": "string"}, + "files": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/definitions/conflict-text-with-sections" }, + { "$ref": "#/definitions/conflict-text-for-editor" } + ] + } + } + }, + "definitions": { + "conflict-base": { + "type": "object", + "required": [ + "old_path", + "new_path", + "blob_icon", + "blob_path" + ], + "properties": { + "old_path": {"type": "string"}, + "new_path": {"type": "string"}, + "blob_icon": {"type": "string"}, + "blob_path": {"type": "string"} + } + }, + "conflict-text-for-editor": { + "allOf": [ + {"$ref": "#/definitions/conflict-base"}, + { + "type": "object", + "required": [ + "type", + "content_path" + ], + "properties": { + "type": {"type": {"enum": ["text-editor"]}}, + "content_path": {"type": "string"} + } + } + ] + }, + "conflict-text-with-sections": { + "allOf": [ + {"$ref": "#/definitions/conflict-base"}, + { + "type": "object", + "required": [ + "type", + "content_path", + "sections" + ], + "properties": { + "type": {"type": {"enum": ["text"]}}, + "content_path": {"type": "string"}, + "sections": { + "type": "array", + "items": { + "oneOf": [ + { "$ref": "#/definitions/section-context" }, + { "$ref": "#/definitions/section-conflict" } + ] + } + } + } + } + ] + }, + "section-base": { + "type": "object", + "required": [ + "conflict", + "lines" + ], + "properties": { + "conflict": {"type": "boolean"}, + "lines": { + "type": "array", + "items": { + "type": "object", + "required": [ + "old_line", + "new_line", + "text", + "rich_text" + ], + "properties": { + "type": {"type": "string"}, + "old_line": {"type": "string"}, + "new_line": {"type": "string"}, + "text": {"type": "string"}, + "rich_text": {"type": "string"} + } + } + } + } + }, + "section-context": { + "allOf": [ + {"$ref": "#/definitions/section-base"}, + { + "type": "object", + "properties": { + "conflict": {"enum": [false]} + } + } + ] + }, + "section-conflict": { + "allOf": [ + {"$ref": "#/definitions/section-base"}, + { + "type": "object", + "required": ["id"], + "properties": { + "conflict": {"enum": [true]}, + "id": {"type": "string"} + } + } + ] + } + } +} -- cgit v1.2.1 From 4743d19463e7aef965665e43238af73820d18d7f Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Wed, 7 Sep 2016 12:28:13 +0100 Subject: Simplify conflict file JSON creation --- lib/gitlab/conflict/file.rb | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb index 26a9f170298..661e43d3fa9 100644 --- a/lib/gitlab/conflict/file.rb +++ b/lib/gitlab/conflict/file.rb @@ -9,7 +9,7 @@ module Gitlab CONTEXT_LINES = 3 - attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository, :type + attr_reader :merge_file_result, :their_path, :our_path, :our_mode, :merge_request, :repository def initialize(merge_file_result, conflict, merge_request:) @merge_file_result = merge_file_result @@ -25,6 +25,12 @@ module Gitlab merge_file_result[:data] end + def type + lines unless @type + + @type.inquiry + end + # Array of Gitlab::Diff::Line objects def lines return @lines if defined?(@lines) @@ -200,12 +206,14 @@ module Gitlab ::File.join(merge_request.diff_refs.head_sha, our_path)) } - if opts[:full_content] - json_hash.merge(content: content) - else - json_hash.merge!(sections: sections) if type == 'text' - - json_hash.merge(type: type, content_path: content_path) + json_hash.tap do |json_hash| + if opts[:full_content] + json_hash[:content] = content + else + json_hash[:sections] = sections if type.text? + json_hash[:type] = type + json_hash[:content_path] = content_path + end end end -- cgit v1.2.1 From 6af52d7d23cf9dbfcd58a2d3031ed19887f7a558 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Tue, 20 Sep 2016 09:16:35 +0300 Subject: We now support resolving conflicts with ambiguous markers --- spec/features/merge_requests/conflicts_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 759edf8ec80..1e8aeacea05 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -42,7 +42,6 @@ feature 'Merge request conflict resolution', js: true, feature: true do UNRESOLVABLE_CONFLICTS = { 'conflict-too-large' => 'when the conflicts contain a large file', 'conflict-binary-file' => 'when the conflicts contain a binary file', - 'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers', 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another', 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file', } -- cgit v1.2.1 From 26f658decd3943bbc66c567ea91e7b859b32e0e6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 8 Sep 2016 11:44:04 -0500 Subject: Implement editor to manually resolve merge conflicts --- .../merge_conflict_data_provider.js.es6 | 165 ++++++++++++++++----- .../javascripts/merge_conflict_resolver.js.es6 | 60 ++++++-- .../components/diff_file_editor.js.es6 | 63 ++++++++ app/assets/stylesheets/pages/merge_conflicts.scss | 26 ++++ .../projects/merge_requests/conflicts.html.haml | 3 + .../conflicts/_diff_file_editor.html.haml | 9 ++ .../conflicts/_file_actions.html.haml | 12 ++ .../conflicts/_inline_view.html.haml | 10 +- .../conflicts/_parallel_view.html.haml | 10 +- .../conflicts/_submit_form.html.haml | 9 +- .../components/_diff_file_editor.html.haml | 6 + 11 files changed, 300 insertions(+), 73 deletions(-) create mode 100644 app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 create mode 100644 app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml create mode 100644 app/views/projects/merge_requests/conflicts/_file_actions.html.haml create mode 100644 app/views/projects/merge_requests/conflicts/components/_diff_file_editor.html.haml 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' } -- cgit v1.2.1 From a3eb39a1068df93d732585272c5eaff380801430 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 16 Sep 2016 19:41:15 -0500 Subject: Replace textarea with Ace editor --- .../components/diff_file_editor.js.es6 | 27 ++++++++++++++++------ app/assets/stylesheets/pages/merge_conflicts.scss | 7 ++++++ .../projects/merge_requests/conflicts.html.haml | 2 ++ .../components/_diff_file_editor.html.haml | 2 +- 4 files changed, 30 insertions(+), 8 deletions(-) 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 index 570d9ff877c..abdf73febb4 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -4,7 +4,7 @@ template: '#diff-file-editor', data() { return { - originalState: '', + originalContent: '', saved: false, loading: false, fileLoaded: false @@ -23,6 +23,8 @@ loadFile(val) { const self = this; + this.resetEditorContent(); + if (!val || this.fileLoaded || this.loading) { return } @@ -31,10 +33,19 @@ $.get(this.file.content_path) .done((file) => { - $(self.$el).find('textarea').val(file.content); - self.originalState = file.content; + let content = self.$el.querySelector('pre'); + let fileContent = document.createTextNode(file.content); + + content.textContent = fileContent.textContent; + + self.originalContent = file.content; self.fileLoaded = true; + self.editor = ace.edit(content); + self.editor.$blockScrolling = Infinity; // Turn off annoying warning + self.editor.on('change', () => { + self.saveDiffResolution(); + }); self.saveDiffResolution(); }) .fail(() => { @@ -50,12 +61,14 @@ 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.content = this.editor.getValue(); + this.file.resolveEditChanged = this.file.content !== this.originalContent; this.file.promptDiscardConfirmation = false; }, - onInput() { - this.saveDiffResolution(); + resetEditorContent() { + if (this.fileLoaded) { + this.editor.setValue(this.originalContent, -1); + } } } }); diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 577a97e8c0e..2a8c693b2a8 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -255,6 +255,13 @@ $colors: ( .editor { border-top: solid 1px yellow; + + pre { + height: 350px; + border: none; + border-radius: 0; + margin-bottom: 0; + } } .loading-text { diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index e3add1f799f..ff641b90b86 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -6,6 +6,8 @@ 'unselected': line.isUnselected }" - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" .merge-request-details.issuable-details 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 index 0556341fd64..940558e02e0 100644 --- 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 @@ -3,4 +3,4 @@ .editor-wrap{ ":class" => "classObject" } %p.loading-text Loading... .editor - %textarea{ "@input" => "onInput", cols: '80', rows: '20' } + %pre{ "style" => "height: 350px" } -- cgit v1.2.1 From 17830296a6143c4d76155449ff4da4377e263657 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 19 Sep 2016 10:10:17 -0500 Subject: Styles for discard alert --- app/assets/stylesheets/framework/variables.scss | 1 + app/assets/stylesheets/pages/merge_conflicts.scss | 16 +++++++++++++--- .../merge_requests/conflicts/_diff_file_editor.html.haml | 7 ++++--- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4c34ed3ebf7..7690d65de8e 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -56,6 +56,7 @@ $border-gray-light: #dcdcdc; $border-gray-normal: #d7d7d7; $border-gray-dark: #c6cacf; +$border-green-extra-light: #9adb84; $border-green-light: #2faa60; $border-green-normal: #2ca05b; $border-green-dark: #279654; diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index 2a8c693b2a8..ea848b2c2c0 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -249,13 +249,11 @@ $colors: ( &.saved { .editor { - border-top: solid 1px green; + border-top: solid 2px $border-green-extra-light; } } .editor { - border-top: solid 1px yellow; - pre { height: 350px; border: none; @@ -268,4 +266,16 @@ $colors: ( display: none; } } + + .discard-changes-alert { + background-color: $background-color; + text-align: right; + padding: $gl-padding-top $gl-padding; + color: $gl-text-color; + + .discard-actions { + display: inline-block; + margin-left: 10px; + } + } } 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 index d0c518e5249..a335470de75 100644 --- a/app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml +++ b/app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml @@ -2,8 +2,9 @@ .diff-editor-wrap{ "v-show" => if_condition } .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" } - %p + .discard-changes-alert 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 + .discard-actions + %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 } -- cgit v1.2.1 From 02bfb0ff1bf53abf44c50d8310ad0856d26860cf Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 19 Sep 2016 10:51:09 -0500 Subject: Style for resolve conflicts form --- app/assets/stylesheets/pages/merge_conflicts.scss | 4 ++++ .../conflicts/_submit_form.html.haml | 27 ++++++++++++++-------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index ea848b2c2c0..c851bd52b1a 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -278,4 +278,8 @@ $colors: ( margin-left: 10px; } } + + .resolve-conflicts-form { + padding-top: $gl-padding; + } } 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 380f8722186..fbb15c307c4 100644 --- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml +++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml @@ -1,10 +1,17 @@ -.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", "@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" +.form-horizontal.resolve-conflicts-form + .form-group + %label.col-sm-2.control-label{ "for" => "commit-message" } + Commit message + .col-sm-10 + .commit-message-container + .max-width-marker + %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" } + {{{conflictsData.commitMessage}}} + .form-group + .col-sm-offset-2.col-sm-10 + .row + .col-xs-6 + %button{ type: "button", class: "btn btn-success js-submit-button", "@click" => "commit()", ":disabled" => "!readyToCommit" } + %span {{commitButtonText}} + .col-xs-6.text-right + = link_to "Cancel", namespace_project_merge_request_path(@merge_request.project.namespace, @merge_request.project, @merge_request), class: "btn btn-cancel" -- cgit v1.2.1 From 0402a18367a7a08d3f40f8b63b961a0e1abb345a Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 19 Sep 2016 11:04:52 -0500 Subject: Replace loading text with spinner --- app/assets/stylesheets/pages/merge_conflicts.scss | 4 ++-- .../merge_requests/conflicts/components/_diff_file_editor.html.haml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss index c851bd52b1a..d447ca53c16 100644 --- a/app/assets/stylesheets/pages/merge_conflicts.scss +++ b/app/assets/stylesheets/pages/merge_conflicts.scss @@ -242,7 +242,7 @@ $colors: ( display: none; } - .loading-text { + .loading { display: block; } } @@ -262,7 +262,7 @@ $colors: ( } } - .loading-text { + .loading { display: none; } } 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 index 940558e02e0..9965bdf9028 100644 --- 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 @@ -1,6 +1,7 @@ %template{ id: "diff-file-editor" } %div .editor-wrap{ ":class" => "classObject" } - %p.loading-text Loading... + .loading + %i.fa.fa-spinner.fa-spin .editor %pre{ "style" => "height: 350px" } -- cgit v1.2.1 From 12ca16080b86b2b0cee8037800952347c2dcd8c4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 20 Sep 2016 12:37:53 -0500 Subject: check if files is set before counting --- app/assets/javascripts/merge_conflict_data_provider.js.es6 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6 index bfed78ff99c..6d1d3f36b33 100644 --- a/app/assets/javascripts/merge_conflict_data_provider.js.es6 +++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6 @@ -268,6 +268,10 @@ class MergeConflictDataProvider { getConflictsCount() { + if (!this.vueInstance.conflictsData.files) { + return 0; + } + const files = this.vueInstance.conflictsData.files; let count = 0; -- cgit v1.2.1 From 4178ddee18918c5186ba75aaf9b303138fb97b30 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Sun, 25 Sep 2016 15:45:58 -0500 Subject: Add tests to check if files are resolved with Edit Inline mode --- spec/features/merge_requests/conflicts_spec.rb | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 1e8aeacea05..d2057aa2fe7 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -36,6 +36,32 @@ feature 'Merge request conflict resolution', js: true, feature: true do retry end end + + context 'when in inline mode' do + it 'resolves files manually' do + within find('.files-wrapper .diff-file.inline-view', text: 'files/ruby/popen.rb') do + click_button 'Edit inline' + wait_for_ajax + execute_script('ace.edit($(".files-wrapper .diff-file.inline-view pre")[0]).setValue("One morning");'); + end + + within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do + click_button 'Edit inline' + wait_for_ajax + execute_script('ace.edit($(".files-wrapper .diff-file.inline-view pre")[1]).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') + + click_on 'Changes' + wait_for_ajax + + expect(page).to have_content('One morning') + expect(page).to have_content('Gregor Samsa woke from troubled dreams') + end + end end end -- cgit v1.2.1 From e84f959ae47e35eaebdc6c0adaf1e089326601ce Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Mon, 26 Sep 2016 15:49:04 +0100 Subject: Fix editor spec --- spec/features/merge_requests/conflicts_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index d2057aa2fe7..721360b207e 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -54,6 +54,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do 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 -- cgit v1.2.1 From a8ac9089afb664e569b34c61dc6782d20d1019d1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 28 Sep 2016 05:12:13 -0500 Subject: Refactor JS code - Use a store base object to manage application state. - Add a service to handle ajax requests. - Load code only when needed --- app/assets/javascripts/dispatcher.js | 3 - .../components/diff_file_editor.js.es6 | 5 +- .../merge_conflicts/merge_conflict_service.js.es6 | 30 ++ .../merge_conflicts/merge_conflict_store.js.es6 | 419 +++++++++++++++++++++ .../merge_conflicts/merge_conflicts_bundle.js.es6 | 84 +++++ .../projects/merge_requests/conflicts.html.haml | 3 +- .../conflicts/_commit_stats.html.haml | 6 +- .../conflicts/_submit_form.html.haml | 1 - config/application.rb | 1 + spec/features/merge_requests/conflicts_spec.rb | 4 +- 10 files changed, 545 insertions(+), 11 deletions(-) create mode 100644 app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index f3ef13ce20e..2c277766d2d 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -97,9 +97,6 @@ new ZenMode(); new MergedButtons(); break; - case "projects:merge_requests:conflicts": - window.mcui = new MergeConflictResolver() - break; case 'projects:merge_requests:index': shortcut_handler = new ShortcutsNavigation(); Issuable.init(); 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 index abdf73febb4..49d30f6e9e8 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -1,5 +1,8 @@ ((global) => { - global.diffFileEditor = Vue.extend({ + + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.diffFileEditor = Vue.extend({ props: ['file', 'loadFile'], template: '#diff-file-editor', data() { diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 new file mode 100644 index 00000000000..da2fb8b1323 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 @@ -0,0 +1,30 @@ +((global) => { + global.mergeConflicts = global.mergeConflicts || {}; + + class mergeConflictsService { + constructor(options) { + this.conflictsPath = options.conflictsPath; + this.resolveConflictsPath = options.resolveConflictsPath; + } + + fetchConflictsData() { + return $.ajax({ + dataType: 'json', + url: this.conflictsPath + }); + } + + submitResolveConflicts(data) { + return $.ajax({ + url: this.resolveConflictsPath, + data: JSON.stringify(data), + contentType: 'application/json', + dataType: 'json', + method: 'POST' + }); + } + }; + + global.mergeConflicts.mergeConflictsService = mergeConflictsService; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 new file mode 100644 index 00000000000..d35e6d8aed6 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -0,0 +1,419 @@ +((global) => { + global.mergeConflicts = global.mergeConflicts || {}; + + const diffViewType = $.cookie('diff_view'); + 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; + const VIEW_TYPES = { + INLINE: 'inline', + PARALLEL: 'parallel' + }; + + global.mergeConflicts.mergeConflictsStore = { + state: { + isLoading: true, + hasError: false, + isSubmitting: false, + isParallel: diffViewType === VIEW_TYPES.PARALLEL, + diffViewType: diffViewType, + conflictsData: {} + }, + + setConflictsData(data) { + this.decorateFiles(data.files); + this.setInlineLines(data.files); + this.setParallelLines(data.files); + + this.state.conflictsData = { + files: data.files, + commitMessage: data.commit_message, + sourceBranch: data.source_branch, + targetBranch: data.target_branch, + commitMessage: data.commit_message, + shortCommitSha: data.commit_sha.slice(0, 7), + }; + }, + + decorateFiles(files) { + files.forEach((file) => { + file.content = ''; + file.resolutionData = {}; + file.promptDiscardConfirmation = false; + file.resolveMode = DEFAULT_RESOLVE_MODE; + }); + }, + + setInlineLines(files) { + files.forEach((file) => { + file.iconClass = `fa-${file.blob_icon}`; + file.blobPath = file.blob_path; + file.filePath = this.getFilePath(file); + file.inlineLines = []; + + file.sections.forEach((section) => { + let currentLineType = 'new'; + const { conflict, lines, id } = section; + + if (conflict) { + file.inlineLines.push(this.getHeadHeaderLine(id)); + } + + lines.forEach((line) => { + const { type } = line; + + if ((type === 'new' || type === 'old') && currentLineType !== type) { + 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) { + files.forEach((file) => { + file.filePath = this.getFilePath(file); + 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) { + linesObj.left.push(this.getOriginHeaderLine(id)); + linesObj.right.push(this.getHeadHeaderLine(id)); + } + + lines.forEach((line) => { + const { type } = line; + + if (conflict) { + 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'; + + linesObj.left.push (this.getLineForParallelView(line, id, lineType)); + linesObj.right.push(this.getLineForParallelView(line, id, lineType, true)); + } + }); + + this.checkLineLengths(linesObj); + }); + + for (let i = 0, len = linesObj.left.length; i < len; i++) { + file.parallelLines.push([ + linesObj.right[i], + linesObj.left[i] + ]); + } + + return file; + }); + }, + + setLoadingState(state) { + this.state.isLoading = state; + }, + + setErrorState(state) { + this.state.hasError = state; + }, + + setFailedRequest(message) { + console.log('setFailedRequest'); + this.state.hasError = true; + this.state.conflictsData.errorMessage = message; + }, + + getConflictsCount() { + if (!this.state.conflictsData.files) { + return 0; + } + + const files = this.state.conflictsData.files; + let count = 0; + + files.forEach((file) => { + file.sections.forEach((section) => { + if (section.conflict) { + count++; + } + }); + }); + + return count; + }, + + getConflictsCountText() { + const count = this.getConflictsCount(); + const text = count ? 'conflicts' : 'conflict'; + + return `${count} ${text}`; + }, + + setViewType(viewType) { + this.state.diffView = viewType; + this.state.isParallel = viewType === VIEW_TYPES.PARALLEL; + + $.cookie('diff_view', viewType, { + path: gon.relative_url_root || '/' + }); + }, + + getHeadHeaderLine(id) { + return { + id: id, + richText: HEAD_HEADER_TEXT, + buttonTitle: HEAD_BUTTON_TITLE, + type: 'new', + section: 'head', + isHeader: true, + isHead: true, + isSelected: false, + isUnselected: false + } + }, + + decorateLineForInlineView(line, id, conflict) { + const { type } = line; + line.id = id; + line.hasConflict = conflict; + line.isHead = type === 'new'; + line.isOrigin = type === 'old'; + line.hasMatch = type === 'match'; + line.richText = line.rich_text; + line.isSelected = false; + line.isUnselected = false; + }, + + getLineForParallelView(line, id, lineType, isHead) { + const { old_line, new_line, rich_text } = line; + const hasConflict = lineType === 'conflict'; + + return { + id, + lineType, + hasConflict, + isHead: hasConflict && isHead, + isOrigin: hasConflict && !isHead, + hasMatch: lineType === 'match', + lineNumber: isHead ? new_line : old_line, + section: isHead ? 'head' : 'origin', + richText: rich_text, + isSelected: false, + isUnselected: false + } + }, + + getOriginHeaderLine(id) { + return { + id: id, + richText: ORIGIN_HEADER_TEXT, + buttonTitle: ORIGIN_BUTTON_TITLE, + type: 'old', + section: 'origin', + isHeader: true, + isOrigin: true, + isSelected: false, + isUnselected: false + } + }, + + getFilePath(file) { + const { old_path, new_path } = file; + return old_path === new_path ? new_path : `${old_path} → ${new_path}`; + }, + + checkLineLengths(linesObj) { + let { left, right } = linesObj; + + if (left.length !== right.length) { + if (left.length > right.length) { + const diff = left.length - right.length; + for (let i = 0; i < diff; i++) { + right.push({ lineType: 'emptyLine', richText: '' }); + } + } + else { + const diff = right.length - left.length; + for (let i = 0; i < diff; i++) { + left.push({ lineType: 'emptyLine', richText: '' }); + } + } + } + }, + + setPromptConfirmationState(file, state) { + file.promptDiscardConfirmation = state; + }, + + setFileResolveMode(file, mode) { + // 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; + } + }); + }, + + isReadyToCommit() { + const files = this.state.conflictsData.files; + const hasCommitMessage = $.trim(this.state.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 !this.state.isSubmitting && hasCommitMessage && !unresolved; + }, + + getCommitButtonText() { + const initial = 'Commit conflict resolution'; + const inProgress = 'Committing...'; + + return this.state ? this.state.isSubmitting ? inProgress : initial : initial; + }, + + getCommitData() { + let commitData = {}; + + commitData = { + commit_message: this.state.conflictsData.commitMessage, + files: [] + }; + + this.state.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; + }, + + handleSelected(file, sectionId, selection) { + Vue.set(file.resolutionData, sectionId, selection); + + this.state.conflictsData.files.forEach((file) => { + file.inlineLines.forEach((line) => { + if (line.id === sectionId && (line.hasConflict || line.isHeader)) { + this.markLine(line, selection); + } + }); + + file.parallelLines.forEach((lines) => { + const left = lines[0]; + const right = lines[1]; + const hasSameId = right.id === sectionId || left.id === sectionId; + const isLeftMatch = left.hasConflict || left.isHeader; + const isRightMatch = right.hasConflict || right.isHeader; + + if (hasSameId && (isLeftMatch || isRightMatch)) { + this.markLine(left, selection); + this.markLine(right, selection); + } + }) + }); + }, + + markLine(line, selection) { + if (selection === 'head' && line.isHead) { + line.isSelected = true; + line.isUnselected = false; + } + else if (selection === 'origin' && line.isOrigin) { + line.isSelected = true; + line.isUnselected = false; + } + else { + line.isSelected = false; + line.isUnselected = true; + } + }, + + setSubmitState(state) { + this.state.isSubmitting = state; + } + }; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 new file mode 100644 index 00000000000..b5123e22f7a --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -0,0 +1,84 @@ +//= require vue +//= require ./merge_conflict_store +//= require ./merge_conflict_service +//= require ./components/diff_file_editor + +$(() => { + const INTERACTIVE_RESOLVE_MODE = 'interactive'; + const $conflicts = $(document.getElementById('conflicts')); + const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore; + const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({ + conflictsPath: $conflicts.data('conflictsPath'), + resolveConflictsPath: $conflicts.data('resolveConflictsPath') + }); + + gl.MergeConflictsResolverApp = new Vue({ + el: '#conflicts', + data: mergeConflictsStore.state, + components: { + 'diff-file-editor': gl.mergeConflicts.diffFileEditor + }, + computed: { + conflictsCountText() { return mergeConflictsStore.getConflictsCountText() }, + readyToCommit() { return mergeConflictsStore.isReadyToCommit() }, + commitButtonText() { return mergeConflictsStore.getCommitButtonText() } + }, + created() { + mergeConflictsService + .fetchConflictsData() + .done((data) => { + if (data.type === 'error') { + mergeConflictsStore.setFailedRequest(data.message); + } else { + mergeConflictsStore.setConflictsData(data); + } + }) + .error(() => { + mergeConflictsStore.setFailedRequest(); + }) + .always(() => { + mergeConflictsStore.setLoadingState(false); + + this.$nextTick(() => { + $conflicts.find('.js-syntax-highlight').syntaxHighlight(); + }); + }); + }, + methods: { + handleSelected(file, sectionId, selection) { + mergeConflictsStore.handleSelected(file, sectionId, selection); + }, + handleViewTypeChange(viewType) { + mergeConflictsStore.setViewType(viewType); + }, + onClickResolveModeButton(file, mode) { + if (mode === INTERACTIVE_RESOLVE_MODE && file.resolveEditChanged) { + mergeConflictsStore.setPromptConfirmationState(file, true); + return; + } + + mergeConflictsStore.setFileResolveMode(file, mode); + }, + acceptDiscardConfirmation(file) { + mergeConflictsStore.setPromptConfirmationState(file, false); + mergeConflictsStore.setFileResolveMode(file, INTERACTIVE_RESOLVE_MODE); + }, + cancelDiscardConfirmation(file) { + mergeConflictsStore.setPromptConfirmationState(file, false); + }, + commit() { + mergeConflictsStore.setSubmitState(true); + + mergeConflictsService + .submitResolveConflicts(mergeConflictsStore.getCommitData()) + .done((data) => { + window.location.href = data.redirect_to; + }) + .error(() => { + mergeConflictsStore.setSubmitState(false); + new Flash('Failed to save merge conflicts resolutions. Please try again!'); + }); + } + } + }) +}); diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index ff641b90b86..997f40c0588 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -7,6 +7,7 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do + = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" @@ -26,8 +27,8 @@ = render partial: "projects/merge_requests/conflicts/commit_stats" .files-wrapper{"v-if" => "!isLoading && !hasError"} - = 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/parallel_view", locals: { class_bindings: class_bindings } = render partial: "projects/merge_requests/conflicts/submit_form" -# Components diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml index 457c467fba9..a3831d5a34e 100644 --- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml +++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml @@ -13,8 +13,8 @@ .js-toggle-container .commit-stat-summary Showing - %strong.cred {{conflictsCount}} {{conflictsData.conflictsText}} + %strong.cred {{conflictsCountText}} between - %strong {{conflictsData.source_branch}} + %strong {{conflictsData.sourceBranch}} and - %strong {{conflictsData.target_branch}} + %strong {{conflictsData.targetBranch}} 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 fbb15c307c4..6ffaa9ad4d2 100644 --- a/app/views/projects/merge_requests/conflicts/_submit_form.html.haml +++ b/app/views/projects/merge_requests/conflicts/_submit_form.html.haml @@ -6,7 +6,6 @@ .commit-message-container .max-width-marker %textarea.form-control.js-commit-message#commit-message{ "v-model" => "conflictsData.commitMessage", "rows" => "5" } - {{{conflictsData.commitMessage}}} .form-group .col-sm-offset-2.col-sm-10 .row diff --git a/config/application.rb b/config/application.rb index 962ffe0708d..8a9c539cb43 100644 --- a/config/application.rb +++ b/config/application.rb @@ -89,6 +89,7 @@ module Gitlab config.assets.precompile << "profile/profile_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "boards/boards_bundle.js" + config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 721360b207e..f2ff000486b 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -42,13 +42,13 @@ feature 'Merge request conflict resolution', js: true, feature: true do within find('.files-wrapper .diff-file.inline-view', text: 'files/ruby/popen.rb') do click_button 'Edit inline' wait_for_ajax - execute_script('ace.edit($(".files-wrapper .diff-file.inline-view pre")[0]).setValue("One morning");'); + execute_script('ace.edit($(".files-wrapper .diff-file.inline-view pre")[0]).setValue("One morning");') end within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do click_button 'Edit inline' wait_for_ajax - execute_script('ace.edit($(".files-wrapper .diff-file.inline-view pre")[1]).setValue("Gregor Samsa woke from troubled dreams");'); + execute_script('ace.edit($(".files-wrapper .diff-file.inline-view pre")[1]).setValue("Gregor Samsa woke from troubled dreams");') end click_button 'Commit conflict resolution' -- cgit v1.2.1 From 197ac5ebbfd8ca7fbcb79776fd25ca0e213376c4 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 29 Sep 2016 20:26:01 -0500 Subject: Ability to resolve conflicts for files with `text-editor` as conflict type --- .../components/diff_file_editor.js.es6 | 43 ++-- .../merge_conflicts/merge_conflict_store.js.es6 | 240 +++++++++++---------- .../conflicts/_diff_file_editor.html.haml | 6 +- .../conflicts/_file_actions.html.haml | 2 +- .../conflicts/_inline_view.html.haml | 41 ++-- .../conflicts/_parallel_view.html.haml | 2 +- spec/features/merge_requests/conflicts_spec.rb | 38 +++- 7 files changed, 213 insertions(+), 159 deletions(-) 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 index 49d30f6e9e8..46aad9fe33c 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -7,10 +7,10 @@ template: '#diff-file-editor', data() { return { - originalContent: '', saved: false, loading: false, - fileLoaded: false + fileLoaded: false, + originalContent: '', } }, computed: { @@ -23,43 +23,48 @@ } }, watch: { - loadFile(val) { - const self = this; - + ['file.showEditor'](val) { this.resetEditorContent(); if (!val || this.fileLoaded || this.loading) { - return + return; } + this.loadEditor(); + } + }, + ready() { + if (this.file.loadEditor) { + this.loadEditor(); + } + }, + methods: { + loadEditor() { this.loading = true; $.get(this.file.content_path) .done((file) => { - - let content = self.$el.querySelector('pre'); + let content = this.$el.querySelector('pre'); let fileContent = document.createTextNode(file.content); content.textContent = fileContent.textContent; - self.originalContent = file.content; - self.fileLoaded = true; - self.editor = ace.edit(content); - self.editor.$blockScrolling = Infinity; // Turn off annoying warning - self.editor.on('change', () => { - self.saveDiffResolution(); + this.originalContent = file.content; + this.fileLoaded = true; + this.editor = ace.edit(content); + this.editor.$blockScrolling = Infinity; // Turn off annoying warning + this.editor.on('change', () => { + this.saveDiffResolution(); }); - self.saveDiffResolution(); + this.saveDiffResolution(); }) .fail(() => { console.log('error'); }) .always(() => { - self.loading = false; + this.loading = false; }); - } - }, - methods: { + }, saveDiffResolution() { this.saved = true; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index d35e6d8aed6..e1f862797f5 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -13,6 +13,10 @@ INLINE: 'inline', PARALLEL: 'parallel' }; + const CONFLICT_TYPES = { + TEXT: 'text', + TEXT_EDITOR: 'text-editor' + }; global.mergeConflicts.mergeConflictsStore = { state: { @@ -26,8 +30,6 @@ setConflictsData(data) { this.decorateFiles(data.files); - this.setInlineLines(data.files); - this.setParallelLines(data.files); this.state.conflictsData = { files: data.files, @@ -45,90 +47,90 @@ file.resolutionData = {}; file.promptDiscardConfirmation = false; file.resolveMode = DEFAULT_RESOLVE_MODE; - }); - }, - - setInlineLines(files) { - files.forEach((file) => { + file.filePath = this.getFilePath(file); file.iconClass = `fa-${file.blob_icon}`; file.blobPath = file.blob_path; - file.filePath = this.getFilePath(file); - file.inlineLines = []; - file.sections.forEach((section) => { - let currentLineType = 'new'; - const { conflict, lines, id } = section; + if (file.type === CONFLICT_TYPES.TEXT) { + file.showEditor = false; + file.loadEditor = false; - if (conflict) { - file.inlineLines.push(this.getHeadHeaderLine(id)); - } + this.setInlineLine(file); + this.setParallelLine(file); + } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) { + file.showEditor = true; + file.loadEditor = true; + } + }); + }, - lines.forEach((line) => { - const { type } = line; + setInlineLine(file) { + file.inlineLines = []; - if ((type === 'new' || type === 'old') && currentLineType !== type) { - currentLineType = type; - file.inlineLines.push({ lineType: 'emptyLine', richText: '' }); - } + file.sections.forEach((section) => { + let currentLineType = 'new'; + const { conflict, lines, id } = section; + + if (conflict) { + file.inlineLines.push(this.getHeadHeaderLine(id)); + } - this.decorateLineForInlineView(line, id, conflict); - file.inlineLines.push(line); - }) + lines.forEach((line) => { + const { type } = line; - if (conflict) { - file.inlineLines.push(this.getOriginHeaderLine(id)); + if ((type === 'new' || type === 'old') && currentLineType !== type) { + 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) { - files.forEach((file) => { - file.filePath = this.getFilePath(file); - 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; + setParallelLine(file) { + file.parallelLines = []; + const linesObj = { left: [], right: [] }; - if (conflict) { - linesObj.left.push(this.getOriginHeaderLine(id)); - linesObj.right.push(this.getHeadHeaderLine(id)); - } + file.sections.forEach((section) => { + const { conflict, lines, id } = section; - lines.forEach((line) => { - const { type } = line; + if (conflict) { + linesObj.left.push(this.getOriginHeaderLine(id)); + linesObj.right.push(this.getHeadHeaderLine(id)); + } - if (conflict) { - 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'; + lines.forEach((line) => { + const { type } = line; - linesObj.left.push (this.getLineForParallelView(line, id, lineType)); - linesObj.right.push(this.getLineForParallelView(line, id, lineType, true)); + if (conflict) { + 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++) { - file.parallelLines.push([ - linesObj.right[i], - linesObj.left[i] - ]); - } - - return file; + this.checkLineLengths(linesObj); }); + + for (let i = 0, len = linesObj.left.length; i < len; i++) { + file.parallelLines.push([ + linesObj.right[i], + linesObj.left[i] + ]); + } }, setLoadingState(state) { @@ -140,13 +142,12 @@ }, setFailedRequest(message) { - console.log('setFailedRequest'); this.state.hasError = true; this.state.conflictsData.errorMessage = message; }, getConflictsCount() { - if (!this.state.conflictsData.files) { + if (!this.state.conflictsData.files.length) { return 0; } @@ -154,11 +155,15 @@ let count = 0; files.forEach((file) => { - file.sections.forEach((section) => { - if (section.conflict) { - count++; - } - }); + if (file.type === CONFLICT_TYPES.TEXT) { + file.sections.forEach((section) => { + if (section.conflict) { + count++; + } + }); + } else { + count++; + } }); return count; @@ -172,7 +177,7 @@ }, setViewType(viewType) { - this.state.diffView = viewType; + this.state.diffView = viewType; this.state.isParallel = viewType === VIEW_TYPES.PARALLEL; $.cookie('diff_view', viewType, { @@ -253,8 +258,7 @@ for (let i = 0; i < diff; i++) { right.push({ lineType: 'emptyLine', richText: '' }); } - } - else { + } else { const diff = right.length - left.length; for (let i = 0; i < diff; i++) { left.push({ lineType: 'emptyLine', richText: '' }); @@ -268,8 +272,12 @@ }, setFileResolveMode(file, mode) { - // Restore Interactive mode when switching to Edit mode - if (mode === EDIT_RESOLVE_MODE) { + if (mode === INTERACTIVE_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 = {}; this.restoreFileLinesState(file); @@ -287,9 +295,9 @@ }); file.parallelLines.forEach((lines) => { - const left = lines[0]; - const right = lines[1]; - const isLeftMatch = left.hasConflict || left.isHeader; + const left = lines[0]; + const right = lines[1]; + const isLeftMatch = left.hasConflict || left.isHeader; const isRightMatch = right.hasConflict || right.isHeader; if (isLeftMatch || isRightMatch) { @@ -313,14 +321,17 @@ 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++; + // We only check if + if (file.type === CONFLICT_TYPES.TEXT) { + for (let j = 0, k = file.sections.length; j < k; j++) { + if (file.sections[j].conflict) { + numberConflicts++; + } } - } - if (resolvedConflicts !== numberConflicts) { - unresolved++; + if (resolvedConflicts !== numberConflicts) { + unresolved++; + } } } else if (file.resolveMode === EDIT_RESOLVE_MODE) { // Unlikely to happen since switching to Edit mode saves content automatically. @@ -358,10 +369,15 @@ 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) { + if (file.type === CONFLICT_TYPES.TEXT) { + + // 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; + } + } else if (file.type === CONFLICT_TYPES.TEXT_EDITOR) { addFile.content = file.content; } @@ -374,39 +390,35 @@ handleSelected(file, sectionId, selection) { Vue.set(file.resolutionData, sectionId, selection); - this.state.conflictsData.files.forEach((file) => { - file.inlineLines.forEach((line) => { - if (line.id === sectionId && (line.hasConflict || line.isHeader)) { - this.markLine(line, selection); - } - }); + file.inlineLines.forEach((line) => { + if (line.id === sectionId && (line.hasConflict || line.isHeader)) { + this.markLine(line, selection); + } + }); - file.parallelLines.forEach((lines) => { - const left = lines[0]; - const right = lines[1]; - const hasSameId = right.id === sectionId || left.id === sectionId; - const isLeftMatch = left.hasConflict || left.isHeader; - const isRightMatch = right.hasConflict || right.isHeader; + file.parallelLines.forEach((lines) => { + const left = lines[0]; + const right = lines[1]; + const hasSameId = right.id === sectionId || left.id === sectionId; + const isLeftMatch = left.hasConflict || left.isHeader; + const isRightMatch = right.hasConflict || right.isHeader; - if (hasSameId && (isLeftMatch || isRightMatch)) { - this.markLine(left, selection); - this.markLine(right, selection); - } - }) + if (hasSameId && (isLeftMatch || isRightMatch)) { + this.markLine(left, selection); + this.markLine(right, selection); + } }); }, markLine(line, selection) { if (selection === 'head' && line.isHead) { - line.isSelected = true; + line.isSelected = true; line.isUnselected = false; - } - else if (selection === 'origin' && line.isOrigin) { - line.isSelected = true; + } else if (selection === 'origin' && line.isOrigin) { + line.isSelected = true; line.isUnselected = false; - } - else { - line.isSelected = false; + } else { + line.isSelected = false; line.isUnselected = true; } }, 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 index a335470de75..497db7e3f9b 100644 --- a/app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml +++ b/app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml @@ -1,10 +1,8 @@ -- if_condition = local_assigns.fetch(:if_condition, '') - -.diff-editor-wrap{ "v-show" => if_condition } +.diff-editor-wrap{ "v-show" => "file.showEditor" } .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" } .discard-changes-alert Are you sure to discard your changes? .discard-actions %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-file-editor{":file" => "file"} diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml index 124dfde7b22..05af57acf03 100644 --- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml +++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml @@ -1,5 +1,5 @@ .file-actions - .btn-group + .btn-group{"v-if" => "file.type === 'text'"} %button.btn{ ":class" => "{ 'active': file.resolveMode == 'interactive' }", '@click' => "onClickResolveModeButton(file, 'interactive')", type: 'button' } 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 7120b6ff48d..60ac21d26c3 100644 --- a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml +++ b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml @@ -4,23 +4,26 @@ %i.fa.fa-fw{":class" => "file.iconClass"} %strong {{file.filePath}} = render partial: 'projects/merge_requests/conflicts/file_actions' - .diff-content.diff-wrap-lines - .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"} - %td.diff-line-num.new_line{":class" => class_bindings} - %a {{line.new_line}} - %td.diff-line-num.old_line{":class" => class_bindings} - %a {{line.old_line}} - %td.line_content{":class" => class_bindings} - {{{line.richText}}} + %template{"v-if" => "file.type === 'text'"} + .diff-content.diff-wrap-lines + .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"} + %td.diff-line-num.new_line{":class" => class_bindings} + %a {{line.new_line}} + %td.diff-line-num.old_line{":class" => class_bindings} + %a {{line.old_line}} + %td.line_content{":class" => class_bindings} + {{{line.richText}}} - %template{"v-if" => "line.isHeader"} - %td.diff-line-num.header{":class" => class_bindings} - %td.diff-line-num.header{":class" => class_bindings} - %td.line_content.header{":class" => class_bindings} - %strong {{{line.richText}}} - %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" } + %template{"v-if" => "line.isHeader"} + %td.diff-line-num.header{":class" => class_bindings} + %td.diff-line-num.header{":class" => class_bindings} + %td.line_content.header{":class" => class_bindings} + %strong {{{line.richText}}} + %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } + {{line.buttonTitle}} + = render partial: 'projects/merge_requests/conflicts/diff_file_editor' + %template{"v-else" => true} + = render partial: 'projects/merge_requests/conflicts/diff_file_editor' 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 18c830ddb17..7ed1485fc01 100644 --- a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml +++ b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml @@ -22,4 +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" } + = render partial: 'projects/merge_requests/conflicts/diff_file_editor', locals: { if_condition: "file.loadFile && isParallel" } diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index f2ff000486b..0e5507a0210 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -37,7 +37,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do end end - context 'when in inline mode' do + context 'when in inline mode' do it 'resolves files manually' do within find('.files-wrapper .diff-file.inline-view', text: 'files/ruby/popen.rb') do click_button 'Edit inline' @@ -66,6 +66,42 @@ feature 'Merge request conflict resolution', js: true, feature: true do 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 = { 'conflict-too-large' => 'when the conflicts contain a large file', 'conflict-binary-file' => 'when the conflicts contain a binary file', -- cgit v1.2.1 From cebad0fb60c90ff64cf16b022d262381aff5bf4f Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 30 Sep 2016 16:23:38 -0500 Subject: Do not show Diff view switcher if all files are can be only resolved with an editor --- .../javascripts/merge_conflicts/merge_conflict_store.js.es6 | 12 ++++++++++++ .../merge_conflicts/merge_conflicts_bundle.js.es6 | 3 ++- .../merge_requests/conflicts/_commit_stats.html.haml | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index e1f862797f5..e31cac49d44 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -425,6 +425,18 @@ setSubmitState(state) { this.state.isSubmitting = state; + }, + + fileTextTypePresent() { + let found = false; + + this.state.conflictsData.files.find((f) => { + if (f.type === CONFLICT_TYPES.TEXT) { + return found = true; + } + }); + + return found; } }; diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index b5123e22f7a..449b4d2209a 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -21,7 +21,8 @@ $(() => { computed: { conflictsCountText() { return mergeConflictsStore.getConflictsCountText() }, readyToCommit() { return mergeConflictsStore.isReadyToCommit() }, - commitButtonText() { return mergeConflictsStore.getCommitButtonText() } + commitButtonText() { return mergeConflictsStore.getCommitButtonText() }, + showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent() } }, created() { mergeConflictsService diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml index a3831d5a34e..7222b8ae4d3 100644 --- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml +++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml @@ -1,5 +1,5 @@ .content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"} - .inline-parallel-buttons + .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"} .btn-group %a.btn{ | ":class" => "{'active': !isParallel}", | -- cgit v1.2.1 From f947972dede2f8d68215555e4b4fd6e90a6bc437 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 30 Sep 2016 17:42:36 -0500 Subject: Improve diff view switching and components --- .../projects/merge_requests/conflicts.html.haml | 17 +++++++++++-- .../conflicts/_inline_view.html.haml | 29 ---------------------- .../conflicts/_parallel_view.html.haml | 25 ------------------- .../_resolve_mode_interactive_inline.html.haml | 17 +++++++++++++ .../_resolve_mode_interactive_parallel.html.haml | 17 +++++++++++++ spec/features/merge_requests/conflicts_spec.rb | 10 ++++---- 6 files changed, 54 insertions(+), 61 deletions(-) delete mode 100644 app/views/projects/merge_requests/conflicts/_inline_view.html.haml delete mode 100644 app/views/projects/merge_requests/conflicts/_parallel_view.html.haml create mode 100644 app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_inline.html.haml create mode 100644 app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_parallel.html.haml diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index 997f40c0588..5ff7befe0ca 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -27,9 +27,22 @@ = render partial: "projects/merge_requests/conflicts/commit_stats" .files-wrapper{"v-if" => "!isLoading && !hasError"} - = render partial: "projects/merge_requests/conflicts/inline_view", locals: { class_bindings: class_bindings } - = render partial: "projects/merge_requests/conflicts/parallel_view", locals: { class_bindings: class_bindings } + .files + .diff-file.file-holder.conflict{"v-for" => "file in conflictsData.files"} + .file-title + %i.fa.fa-fw{":class" => "file.iconClass"} + %strong {{file.filePath}} + = render partial: 'projects/merge_requests/conflicts/file_actions' + .diff-content.diff-wrap-lines + %div{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + = render partial: "projects/merge_requests/conflicts/resolve_mode_interactive_inline", locals: { class_bindings: class_bindings } + %div{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + = render partial: "projects/merge_requests/conflicts/resolve_mode_interactive_parallel", locals: { class_bindings: class_bindings } + %div{"v-show" => " file.resolveMode === 'edit' || file.type === 'text-editor'"} + = render partial: 'projects/merge_requests/conflicts/diff_file_editor', locals: { if_condition: "file.loadFile && isParallel" } + = render partial: "projects/merge_requests/conflicts/submit_form" -# Components = render partial: 'projects/merge_requests/conflicts/components/diff_file_editor' + \ No newline at end of file diff --git a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml b/app/views/projects/merge_requests/conflicts/_inline_view.html.haml deleted file mode 100644 index 60ac21d26c3..00000000000 --- a/app/views/projects/merge_requests/conflicts/_inline_view.html.haml +++ /dev/null @@ -1,29 +0,0 @@ -.files{"v-show" => "!isParallel"} - .diff-file.file-holder.conflict.inline-view{"v-for" => "file in conflictsData.files"} - .file-title - %i.fa.fa-fw{":class" => "file.iconClass"} - %strong {{file.filePath}} - = render partial: 'projects/merge_requests/conflicts/file_actions' - %template{"v-if" => "file.type === 'text'"} - .diff-content.diff-wrap-lines - .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"} - %td.diff-line-num.new_line{":class" => class_bindings} - %a {{line.new_line}} - %td.diff-line-num.old_line{":class" => class_bindings} - %a {{line.old_line}} - %td.line_content{":class" => class_bindings} - {{{line.richText}}} - - %template{"v-if" => "line.isHeader"} - %td.diff-line-num.header{":class" => class_bindings} - %td.diff-line-num.header{":class" => class_bindings} - %td.line_content.header{":class" => class_bindings} - %strong {{{line.richText}}} - %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } - {{line.buttonTitle}} - = render partial: 'projects/merge_requests/conflicts/diff_file_editor' - %template{"v-else" => true} - = render partial: 'projects/merge_requests/conflicts/diff_file_editor' diff --git a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml b/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml deleted file mode 100644 index 7ed1485fc01..00000000000 --- a/app/views/projects/merge_requests/conflicts/_parallel_view.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -.files{"v-show" => "isParallel"} - .diff-file.file-holder.conflict.parallel-view{"v-for" => "file in conflictsData.files"} - .file-title - %i.fa.fa-fw{":class" => "file.iconClass"} - %strong {{file.filePath}} - = render partial: 'projects/merge_requests/conflicts/file_actions' - .diff-content.diff-wrap-lines - .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"} - - %template{"v-if" => "line.isHeader"} - %td.diff-line-num.header{":class" => class_bindings} - %td.line_content.header{":class" => class_bindings} - %strong {{line.richText}} - %button.btn{"@click" => "handleSelected(file, line.id, line.section)"} - {{line.buttonTitle}} - - %template{"v-if" => "!line.isHeader"} - %td.diff-line-num.old_line{":class" => class_bindings} - {{line.lineNumber}} - %td.line_content.parallel{":class" => class_bindings} - {{{line.richText}}} - = render partial: 'projects/merge_requests/conflicts/diff_file_editor', locals: { if_condition: "file.loadFile && isParallel" } diff --git a/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_inline.html.haml b/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_inline.html.haml new file mode 100644 index 00000000000..b1d7a90eb74 --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_inline.html.haml @@ -0,0 +1,17 @@ +.diff-wrap-lines.code.file-content.js-syntax-highlight + %table + %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"} + %template{"v-if" => "!line.isHeader"} + %td.diff-line-num.new_line{":class" => class_bindings} + %a {{line.new_line}} + %td.diff-line-num.old_line{":class" => class_bindings} + %a {{line.old_line}} + %td.line_content{":class" => class_bindings} + {{{line.richText}}} + %template{"v-if" => "line.isHeader"} + %td.diff-line-num.header{":class" => class_bindings} + %td.diff-line-num.header{":class" => class_bindings} + %td.line_content.header{":class" => class_bindings} + %strong {{{line.richText}}} + %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } + {{line.buttonTitle}} diff --git a/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_parallel.html.haml b/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_parallel.html.haml new file mode 100644 index 00000000000..f5af347f59b --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_parallel.html.haml @@ -0,0 +1,17 @@ +.diff-wrap-lines.code.file-content.js-syntax-highlight + %table + %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"} + %template{"v-for" => "line in section"} + + %template{"v-if" => "line.isHeader"} + %td.diff-line-num.header{":class" => class_bindings} + %td.line_content.header{":class" => class_bindings} + %strong {{line.richText}} + %button.btn{"@click" => "handleSelected(file, line.id, line.section)"} + {{line.buttonTitle}} + + %template{"v-if" => "!line.isHeader"} + %td.diff-line-num.old_line{":class" => class_bindings} + {{line.lineNumber}} + %td.line_content.parallel{":class" => class_bindings} + {{{line.richText}}} diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 0e5507a0210..4cecc22aa6c 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -39,16 +39,16 @@ feature 'Merge request conflict resolution', js: true, feature: true do context 'when in inline mode' 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', text: 'files/ruby/popen.rb') do click_button 'Edit inline' wait_for_ajax - execute_script('ace.edit($(".files-wrapper .diff-file.inline-view pre")[0]).setValue("One morning");') + execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");') end within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do click_button 'Edit inline' wait_for_ajax - execute_script('ace.edit($(".files-wrapper .diff-file.inline-view pre")[1]).setValue("Gregor Samsa woke from troubled dreams");') + execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");') end click_button 'Commit conflict resolution' @@ -80,9 +80,9 @@ feature 'Merge request conflict resolution', js: true, feature: true 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 + within find('.files-wrapper .diff-file', 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");') + execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");') end click_button 'Commit conflict resolution' -- cgit v1.2.1 From 4f76ff0ecd0d3b278f9722042b66d1767078ddd0 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Tue, 4 Oct 2016 03:24:44 -0500 Subject: Use .some instead of .find for phantomjs compatibility --- .../javascripts/merge_conflicts/merge_conflict_store.js.es6 | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index e31cac49d44..bdd6753f66a 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -428,15 +428,7 @@ }, fileTextTypePresent() { - let found = false; - - this.state.conflictsData.files.find((f) => { - if (f.type === CONFLICT_TYPES.TEXT) { - return found = true; - } - }); - - return found; + return this.state.conflictsData.files.some(f => f.type === CONFLICT_TYPES.TEXT); } }; -- cgit v1.2.1 From c4142cf9c0c0b217034c60a0a973d2e96b17a428 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 5 Oct 2016 07:57:22 -0500 Subject: Improve components for PhantomJs compatibility --- .../components/diff_file_editor.js.es6 | 14 ++++++++++--- .../components/inline_conflict_lines.js.es6 | 12 +++++++++++ .../components/parallel_conflict_line.js.es6 | 14 +++++++++++++ .../components/parallel_conflict_lines.js.es6 | 15 ++++++++++++++ .../merge_conflicts/merge_conflict_store.js.es6 | 6 +++--- .../merge_conflicts/merge_conflicts_bundle.js.es6 | 12 +++++++---- .../mixins/line_conflict_actions.js.es6 | 12 +++++++++++ .../mixins/line_conflict_utils.js.es6 | 18 ++++++++++++++++ .../projects/merge_requests/conflicts.html.haml | 24 ++++++++-------------- .../conflicts/_commit_stats.html.haml | 8 ++------ .../conflicts/_diff_file_editor.html.haml | 8 -------- .../_resolve_mode_interactive_inline.html.haml | 17 --------------- .../_resolve_mode_interactive_parallel.html.haml | 17 --------------- .../components/_diff_file_editor.html.haml | 10 +++++++-- .../components/_inline_conflict_lines.html.haml | 15 ++++++++++++++ .../components/_parallel_conflict_line.html.haml | 10 +++++++++ .../components/_parallel_conflict_lines.html.haml | 4 ++++ 17 files changed, 140 insertions(+), 76 deletions(-) create mode 100644 app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 create mode 100644 app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 delete mode 100644 app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml delete mode 100644 app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_inline.html.haml delete mode 100644 app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_parallel.html.haml create mode 100644 app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml create mode 100644 app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml create mode 100644 app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml 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 index 46aad9fe33c..417095a8db7 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -3,8 +3,11 @@ global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.diffFileEditor = Vue.extend({ - props: ['file', 'loadFile'], - template: '#diff-file-editor', + props: { + file: Object, + onCancelDiscardConfirmation: Function, + onAcceptDiscardConfirmation: Function + }, data() { return { saved: false, @@ -16,7 +19,6 @@ computed: { classObject() { return { - 'load-file': this.loadFile, 'saved': this.saved, 'is-loading': this.loading }; @@ -77,6 +79,12 @@ if (this.fileLoaded) { this.editor.setValue(this.originalContent, -1); } + }, + cancelDiscardConfirmation(file) { + this.onCancelDiscardConfirmation(file); + }, + acceptDiscardConfirmation(file) { + this.onAcceptDiscardConfirmation(file); } } }); diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 new file mode 100644 index 00000000000..b4be1c8988d --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 @@ -0,0 +1,12 @@ +((global) => { + + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.inlineConflictLines = Vue.extend({ + props: { + file: Object + }, + mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], + }); + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 new file mode 100644 index 00000000000..8b0a8ab2073 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 @@ -0,0 +1,14 @@ +((global) => { + + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.parallelConflictLine = Vue.extend({ + props: { + file: Object, + line: Object + }, + mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], + template: '#parallel-conflict-line' + }); + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 new file mode 100644 index 00000000000..eb4cc6a9dac --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 @@ -0,0 +1,15 @@ +((global) => { + + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.parallelConflictLines = Vue.extend({ + props: { + file: Object + }, + mixins: [global.mergeConflicts.utils], + components: { + 'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine + } + }); + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index bdd6753f66a..83bcc3f51aa 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -196,7 +196,7 @@ isHead: true, isSelected: false, isUnselected: false - } + }; }, decorateLineForInlineView(line, id, conflict) { @@ -227,7 +227,7 @@ richText: rich_text, isSelected: false, isUnselected: false - } + }; }, getOriginHeaderLine(id) { @@ -241,7 +241,7 @@ isOrigin: true, isSelected: false, isUnselected: false - } + }; }, getFilePath(file) { diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index 449b4d2209a..12688365f19 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -1,7 +1,12 @@ //= require vue //= require ./merge_conflict_store //= require ./merge_conflict_service +//= require ./mixins/line_conflict_utils +//= require ./mixins/line_conflict_actions //= require ./components/diff_file_editor +//= require ./components/inline_conflict_lines +//= require ./components/parallel_conflict_line +//= require ./components/parallel_conflict_lines $(() => { const INTERACTIVE_RESOLVE_MODE = 'interactive'; @@ -16,7 +21,9 @@ $(() => { el: '#conflicts', data: mergeConflictsStore.state, components: { - 'diff-file-editor': gl.mergeConflicts.diffFileEditor + 'diff-file-editor': gl.mergeConflicts.diffFileEditor, + 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines, + 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines }, computed: { conflictsCountText() { return mergeConflictsStore.getConflictsCountText() }, @@ -46,9 +53,6 @@ $(() => { }); }, methods: { - handleSelected(file, sectionId, selection) { - mergeConflictsStore.handleSelected(file, sectionId, selection); - }, handleViewTypeChange(viewType) { mergeConflictsStore.setViewType(viewType); }, diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 new file mode 100644 index 00000000000..114a2c5b305 --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 @@ -0,0 +1,12 @@ +((global) => { + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.actions = { + methods: { + handleSelected(file, sectionId, selection) { + gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection); + } + } + }; + +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 new file mode 100644 index 00000000000..b846a90ab2a --- /dev/null +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 @@ -0,0 +1,18 @@ +((global) => { + global.mergeConflicts = global.mergeConflicts || {}; + + global.mergeConflicts.utils = { + methods: { + lineCssClass(line) { + return { + 'head': line.isHead, + 'origin': line.isOrigin, + 'match': line.hasMatch, + 'selected': line.isSelected, + 'unselected': line.isUnselected + }; + } + } + }; + +})(window.gl || (window.gl = {})); diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index 5ff7befe0ca..d9f74d2cbfb 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,10 +1,3 @@ -- class_bindings = "{ | - 'head': line.isHead, | - 'origin': line.isOrigin, | - 'match': line.hasMatch, | - 'selected': line.isSelected, | - 'unselected': line.isUnselected }" - - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js') @@ -34,15 +27,14 @@ %strong {{file.filePath}} = render partial: 'projects/merge_requests/conflicts/file_actions' .diff-content.diff-wrap-lines - %div{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } - = render partial: "projects/merge_requests/conflicts/resolve_mode_interactive_inline", locals: { class_bindings: class_bindings } - %div{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } - = render partial: "projects/merge_requests/conflicts/resolve_mode_interactive_parallel", locals: { class_bindings: class_bindings } - %div{"v-show" => " file.resolveMode === 'edit' || file.type === 'text-editor'"} - = render partial: 'projects/merge_requests/conflicts/diff_file_editor', locals: { if_condition: "file.loadFile && isParallel" } + .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + = render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines" + .diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" } + = render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines" + %div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"} + = render partial: "projects/merge_requests/conflicts/components/diff_file_editor" = render partial: "projects/merge_requests/conflicts/submit_form" --# Components -= render partial: 'projects/merge_requests/conflicts/components/diff_file_editor' - \ No newline at end of file +-# Components += render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line' diff --git a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml index 7222b8ae4d3..5ab3cd96163 100644 --- a/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml +++ b/app/views/projects/merge_requests/conflicts/_commit_stats.html.haml @@ -1,13 +1,9 @@ .content-block.oneline-block.files-changed{"v-if" => "!isLoading && !hasError"} .inline-parallel-buttons{"v-if" => "showDiffViewTypeSwitcher"} .btn-group - %a.btn{ | - ":class" => "{'active': !isParallel}", | - "@click" => "handleViewTypeChange('inline')"} + %button.btn{":class" => "{'active': !isParallel}", "@click" => "handleViewTypeChange('inline')"} Inline - %a.btn{ | - ":class" => "{'active': isParallel}", | - "@click" => "handleViewTypeChange('parallel')"} + %button.btn{":class" => "{'active': isParallel}", "@click" => "handleViewTypeChange('parallel')"} Side-by-side .js-toggle-container 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 deleted file mode 100644 index 497db7e3f9b..00000000000 --- a/app/views/projects/merge_requests/conflicts/_diff_file_editor.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.diff-editor-wrap{ "v-show" => "file.showEditor" } - .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" } - .discard-changes-alert - Are you sure to discard your changes? - .discard-actions - %button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes - %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel - %diff-file-editor{":file" => "file"} diff --git a/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_inline.html.haml b/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_inline.html.haml deleted file mode 100644 index b1d7a90eb74..00000000000 --- a/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_inline.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -.diff-wrap-lines.code.file-content.js-syntax-highlight - %table - %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"} - %template{"v-if" => "!line.isHeader"} - %td.diff-line-num.new_line{":class" => class_bindings} - %a {{line.new_line}} - %td.diff-line-num.old_line{":class" => class_bindings} - %a {{line.old_line}} - %td.line_content{":class" => class_bindings} - {{{line.richText}}} - %template{"v-if" => "line.isHeader"} - %td.diff-line-num.header{":class" => class_bindings} - %td.diff-line-num.header{":class" => class_bindings} - %td.line_content.header{":class" => class_bindings} - %strong {{{line.richText}}} - %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } - {{line.buttonTitle}} diff --git a/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_parallel.html.haml b/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_parallel.html.haml deleted file mode 100644 index f5af347f59b..00000000000 --- a/app/views/projects/merge_requests/conflicts/_resolve_mode_interactive_parallel.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -.diff-wrap-lines.code.file-content.js-syntax-highlight - %table - %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"} - %template{"v-for" => "line in section"} - - %template{"v-if" => "line.isHeader"} - %td.diff-line-num.header{":class" => class_bindings} - %td.line_content.header{":class" => class_bindings} - %strong {{line.richText}} - %button.btn{"@click" => "handleSelected(file, line.id, line.section)"} - {{line.buttonTitle}} - - %template{"v-if" => "!line.isHeader"} - %td.diff-line-num.old_line{":class" => class_bindings} - {{line.lineNumber}} - %td.line_content.parallel{":class" => class_bindings} - {{{line.richText}}} 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 index 9965bdf9028..943ae6ee129 100644 --- 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 @@ -1,5 +1,11 @@ -%template{ id: "diff-file-editor" } - %div +%diff-file-editor{"inline-template" => "true", ":file" => "file", ":on-cancel-discard-confirmation" => "cancelDiscardConfirmation", ":on-accept-discard-confirmation" => "acceptDiscardConfirmation"} + .diff-editor-wrap{ "v-show" => "file.showEditor" } + .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" } + .discard-changes-alert + Are you sure to discard your changes? + .discard-actions + %button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes + %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel .editor-wrap{ ":class" => "classObject" } .loading %i.fa.fa-spinner.fa-spin diff --git a/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml new file mode 100644 index 00000000000..f094df7fcaa --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/components/_inline_conflict_lines.html.haml @@ -0,0 +1,15 @@ +%inline-conflict-lines{ "inline-template" => "true", ":file" => "file"} + %table + %tr.line_holder.diff-inline{"v-for" => "line in file.inlineLines"} + %td.diff-line-num.new_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + %a {{line.new_line}} + %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + %a {{line.old_line}} + %td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + {{{line.richText}}} + %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %strong {{{line.richText}}} + %button.btn{ "@click" => "handleSelected(file, line.id, line.section)" } + {{line.buttonTitle}} diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml new file mode 100644 index 00000000000..5690bf7419c --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_line.html.haml @@ -0,0 +1,10 @@ +%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"} + %td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"} + %strong {{line.richText}} + %button.btn{"@click" => "handleSelected(file, line.id, line.section)"} + {{line.buttonTitle}} + %td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + {{line.lineNumber}} + %td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"} + {{{line.richText}}} diff --git a/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml new file mode 100644 index 00000000000..a8ecdf59393 --- /dev/null +++ b/app/views/projects/merge_requests/conflicts/components/_parallel_conflict_lines.html.haml @@ -0,0 +1,4 @@ +%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"} + %table + %tr.line_holder.parallel{"v-for" => "section in file.parallelLines"} + %td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"} -- cgit v1.2.1 From 54bfe70795e289b86485b2a57d72b6711e4994bd Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 5 Oct 2016 07:57:57 -0500 Subject: Add more tests to check conflicts resolution --- .../merge_conflict_data_provider.js.es6 | 2 +- .../merge_conflicts/merge_conflict_store.js.es6 | 4 +- .../projects/merge_requests_controller_spec.rb | 1 + spec/features/merge_requests/conflicts_spec.rb | 145 ++++++++++++++------- 4 files changed, 101 insertions(+), 51 deletions(-) diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6 index 6d1d3f36b33..5877d2f1896 100644 --- a/app/assets/javascripts/merge_conflict_data_provider.js.es6 +++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6 @@ -290,7 +290,7 @@ class MergeConflictDataProvider { isReadyToCommit() { const vi = this.vueInstance; const files = this.vueInstance.conflictsData.files; - const hasCommitMessage = $.trim(this.vueInstance.conflictsData.commitMessage).length; + const hasCommitMessage = this.vueInstance.conflictsData.commitMessage.trim(); let unresolved = 0; for (let i = 0, l = files.length; i < l; i++) { diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 index 83bcc3f51aa..5c5c65f29d4 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 @@ -321,7 +321,8 @@ let numberConflicts = 0; let resolvedConflicts = Object.keys(file.resolutionData).length - // We only check if + // We only check for conflicts type 'text' + // since conflicts `text_editor` can´t be resolved in interactive mode if (file.type === CONFLICT_TYPES.TEXT) { for (let j = 0, k = file.sections.length; j < k; j++) { if (file.sections[j].conflict) { @@ -334,6 +335,7 @@ } } } 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) { diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 06b37aa4997..31f43bdc89a 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -659,6 +659,7 @@ describe Projects::MergeRequestsController do id: merge_request.iid expect(merge_request.reload.title).to eq(merge_request.wipless_title) + end end describe 'GET conflict_for_path' do diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 4cecc22aa6c..d258ff52bbb 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -12,74 +12,121 @@ feature 'Merge request conflict resolution', js: true, feature: true do end end - context 'when a merge request can be resolved in the UI' do - let(:merge_request) { create_merge_request('conflict-resolvable') } + shared_examples "conflicts are resolved in Interactive mode" do + it 'conflicts are resolved in Interactive mode' do + within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do + click_button 'Use ours' + end - before do - project.team << [user, :developer] - login_as(user) + within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do + all('button', text: 'Use ours').each do |button| + button.click + end + end - visit namespace_project_merge_request_path(project.namespace, project, merge_request) - end + click_button 'Commit conflict resolution' + wait_for_ajax - it 'shows a link to the conflict resolution page' do - expect(page).to have_link('conflicts', href: /\/conflicts\Z/) - end + expect(page).to have_content('All merge conflicts were resolved') + merge_request.reload_diff - context 'visiting the conflicts resolution page' do - before { click_link('conflicts', href: /\/conflicts\Z/) } + click_on 'Changes' + wait_for_ajax - it 'shows the conflicts' do - begin - expect(find('#conflicts')).to have_content('popen.rb') - rescue Capybara::Poltergeist::JavascriptError - retry - end + within find('.diff-file', text: 'files/ruby/popen.rb') do + expect(page).to have_selector('.line_content.new', text: "vars = { 'PWD' => path }") + expect(page).to have_selector('.line_content.new', text: "options = { chdir: path }") end - context 'when in inline mode' do - it 'resolves files manually' do - within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do - click_button 'Edit inline' - wait_for_ajax - execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");') - end - - within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do - click_button 'Edit inline' - wait_for_ajax - execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).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 + within find('.diff-file', text: 'files/ruby/regex.rb') do + expect(page).to have_selector('.line_content.new', text: "def username_regexp") + expect(page).to have_selector('.line_content.new', text: "def project_name_regexp") + expect(page).to have_selector('.line_content.new', text: "def path_regexp") + expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp") + expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp") + expect(page).to have_selector('.line_content.new', text: "def default_regexp") + end + end + end - click_on 'Changes' - wait_for_ajax + shared_examples "conflicts are resolved in Edit inline mode" do + it 'conflicts are resolved in Edit inline mode' do + expect(find('#conflicts')).to have_content('popen.rb') - expect(page).to have_content('One morning') - expect(page).to have_content('Gregor Samsa woke from troubled dreams') - end + within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do + click_button 'Edit inline' + wait_for_ajax + execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");') end + + within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do + click_button 'Edit inline' + wait_for_ajax + execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).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 + + expect(page).to have_content('One morning') + expect(page).to have_content('Gregor Samsa woke from troubled dreams') end end - context 'when a merge request can be resolved in the UI' do - let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') } - + context 'can be resolved in the UI' do before do project.team << [user, :developer] login_as(user) + end - visit namespace_project_merge_request_path(project.namespace, project, merge_request) + context 'the conflicts are resolvable' do + let(:merge_request) { create_merge_request('conflict-resolvable') } + + before { visit namespace_project_merge_request_path(project.namespace, project, merge_request) } + + it 'shows a link to the conflict resolution page' do + expect(page).to have_link('conflicts', href: /\/conflicts\Z/) + end + + context 'in Inline view mode' do + before { click_link('conflicts', href: /\/conflicts\Z/) } + + include_examples "conflicts are resolved in Interactive mode" + include_examples "conflicts are resolved in Edit inline mode" + end + + context 'in Parallel view mode' do + before do + click_link('conflicts', href: /\/conflicts\Z/) + click_button 'Side-by-side' + end + + include_examples "conflicts are resolved in Interactive mode" + include_examples "conflicts are resolved in Edit inline mode" + end end - context 'a conflict contain markers' do - before { click_link('conflicts', href: /\/conflicts\Z/) } + context 'the conflict contain markers' do + let(:merge_request) { create_merge_request('conflict-contains-conflict-markers') } + + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + click_link('conflicts', href: /\/conflicts\Z/) + end + + it 'conflicts can not be resolved in Interactive mode' do + within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do + expect(page).not_to have_content 'Interactive mode' + expect(page).not_to have_content 'Edit inline' + end + end - it 'resolves files manually' do + it 'conflicts are resolved in Edit inline mode' do within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do wait_for_ajax execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");') @@ -94,7 +141,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do click_on 'Changes' wait_for_ajax - find('.nothing-here-block', visible: true).click + find('.click-to-expand').click wait_for_ajax expect(page).to have_content('Gregor Samsa woke from troubled dreams') -- cgit v1.2.1 From cc874d9167b7ba5531aeade09f69ecdc1057a0ee Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 5 Oct 2016 18:05:16 -0500 Subject: Fix column limit on mysql --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8708eae1b2a..2c2edd8e9e0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,7 +22,7 @@ before_script: - bundle --version - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"' - retry gem install knapsack - - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate' + - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql' stages: - prepare -- cgit v1.2.1 From 8fd7a5942286d69eb34406e06ba5263c3944012e Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 5 Oct 2016 20:49:16 -0500 Subject: Remove unused files --- .../merge_conflict_data_provider.js.es6 | 438 --------------------- .../javascripts/merge_conflict_resolver.js.es6 | 112 ------ 2 files changed, 550 deletions(-) delete mode 100644 app/assets/javascripts/merge_conflict_data_provider.js.es6 delete mode 100644 app/assets/javascripts/merge_conflict_resolver.js.es6 diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6 deleted file mode 100644 index 5877d2f1896..00000000000 --- a/app/assets/javascripts/merge_conflict_data_provider.js.es6 +++ /dev/null @@ -1,438 +0,0 @@ -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 { - - getInitialData() { - // TODO: remove reliance on jQuery and DOM state introspection - const diffViewType = $.cookie('diff_view'); - const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited'); - - return { - isLoading : true, - hasError : false, - isParallel : diffViewType === 'parallel', - diffViewType : diffViewType, - fixedLayout : fixedLayout, - isSubmitting : false, - conflictsData : {} - } - } - - - decorateData(vueInstance, data) { - this.vueInstance = vueInstance; - - if (data.type === 'error') { - vueInstance.hasError = true; - data.errorMessage = data.message; - } - else { - data.shortCommitSha = data.commit_sha.slice(0, 7); - data.commitMessage = data.commit_message; - - this.decorateFiles(data); - this.setParallelLines(data); - this.setInlineLines(data); - } - - vueInstance.conflictsData = data; - vueInstance.isSubmitting = false; - - const conflictsText = this.getConflictsCount() > 1 ? 'conflicts' : 'conflict'; - vueInstance.conflictsData.conflictsText = conflictsText; - } - - decorateFiles(data) { - data.files.forEach((file) => { - file.content = ''; - file.resolutionData = {}; - file.promptDiscardConfirmation = false; - file.resolveMode = DEFAULT_RESOLVE_MODE; - }); - } - - - setParallelLines(data) { - data.files.forEach( (file) => { - file.filePath = this.getFilePath(file); - 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) { - linesObj.left.push(this.getOriginHeaderLine(id)); - linesObj.right.push(this.getHeadHeaderLine(id)); - } - - lines.forEach( (line) => { - const { type } = line; - - if (conflict) { - 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'; - - linesObj.left.push (this.getLineForParallelView(line, id, lineType)); - linesObj.right.push(this.getLineForParallelView(line, id, lineType, true)); - } - }); - - this.checkLineLengths(linesObj); - }); - - for (let i = 0, len = linesObj.left.length; i < len; i++) { - file.parallelLines.push([ - linesObj.right[i], - linesObj.left[i] - ]); - } - - }); - } - - - checkLineLengths(linesObj) { - let { left, right } = linesObj; - - if (left.length !== right.length) { - if (left.length > right.length) { - const diff = left.length - right.length; - for (let i = 0; i < diff; i++) { - right.push({ lineType: 'emptyLine', richText: '' }); - } - } - else { - const diff = right.length - left.length; - for (let i = 0; i < diff; i++) { - left.push({ lineType: 'emptyLine', richText: '' }); - } - } - } - } - - - setInlineLines(data) { - data.files.forEach( (file) => { - file.iconClass = `fa-${file.blob_icon}`; - file.blobPath = file.blob_path; - file.filePath = this.getFilePath(file); - file.inlineLines = [] - - file.sections.forEach( (section) => { - let currentLineType = 'new'; - const { conflict, lines, id } = section; - - if (conflict) { - file.inlineLines.push(this.getHeadHeaderLine(id)); - } - - lines.forEach( (line) => { - const { type } = line; - - if ((type === 'new' || type === 'old') && currentLineType !== type) { - 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)); - } - }); - }); - } - - - handleSelected(file, sectionId, selection) { - const vi = this.vueInstance; - let files = vi.conflictsData.files; - - vi.$set(`conflictsData.files[${files.indexOf(file)}].resolutionData['${sectionId}']`, selection); - - - files.forEach( (file) => { - file.inlineLines.forEach( (line) => { - if (line.id === sectionId && (line.hasConflict || line.isHeader)) { - this.markLine(line, selection); - } - }); - - file.parallelLines.forEach( (lines) => { - const left = lines[0]; - const right = lines[1]; - const hasSameId = right.id === sectionId || left.id === sectionId; - const isLeftMatch = left.hasConflict || left.isHeader; - const isRightMatch = right.hasConflict || right.isHeader; - - if (hasSameId && (isLeftMatch || isRightMatch)) { - this.markLine(left, selection); - this.markLine(right, selection); - } - }) - }); - } - - - updateViewType(newType) { - const vi = this.vueInstance; - - if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) { - return; - } - - vi.diffViewType = newType; - vi.isParallel = newType === 'parallel'; - $.cookie('diff_view', newType, { - path: (gon && gon.relative_url_root) || '/' - }); - $('.content-wrapper .container-fluid') - .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) { - line.isSelected = true; - line.isUnselected = false; - } - else if (selection === 'origin' && line.isOrigin) { - line.isSelected = true; - line.isUnselected = false; - } - else { - line.isSelected = false; - line.isUnselected = true; - } - } - - - getConflictsCount() { - if (!this.vueInstance.conflictsData.files) { - return 0; - } - - const files = this.vueInstance.conflictsData.files; - let count = 0; - - files.forEach((file) => { - file.sections.forEach((section) => { - if (section.conflict) { - count++; - } - }); - }); - - return count; - } - - - isReadyToCommit() { - const vi = this.vueInstance; - const files = this.vueInstance.conflictsData.files; - const hasCommitMessage = this.vueInstance.conflictsData.commitMessage.trim(); - 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 !vi.isSubmitting && hasCommitMessage && !unresolved; - } - - - getCommitButtonText() { - const initial = 'Commit conflict resolution'; - const inProgress = 'Committing...'; - const vue = this.vueInstance; - - return vue ? vue.isSubmitting ? inProgress : initial : initial; - } - - - decorateLineForInlineView(line, id, conflict) { - const { type } = line; - line.id = id; - line.hasConflict = conflict; - line.isHead = type === 'new'; - line.isOrigin = type === 'old'; - line.hasMatch = type === 'match'; - line.richText = line.rich_text; - line.isSelected = false; - line.isUnselected = false; - } - - getLineForParallelView(line, id, lineType, isHead) { - const { old_line, new_line, rich_text } = line; - const hasConflict = lineType === 'conflict'; - - return { - id, - lineType, - hasConflict, - isHead : hasConflict && isHead, - isOrigin : hasConflict && !isHead, - hasMatch : lineType === 'match', - lineNumber : isHead ? new_line : old_line, - section : isHead ? 'head' : 'origin', - richText : rich_text, - isSelected : false, - isUnselected : false - } - } - - - getHeadHeaderLine(id) { - return { - id : id, - richText : HEAD_HEADER_TEXT, - buttonTitle : HEAD_BUTTON_TITLE, - type : 'new', - section : 'head', - isHeader : true, - isHead : true, - isSelected : false, - isUnselected: false - } - } - - - getOriginHeaderLine(id) { - return { - id : id, - richText : ORIGIN_HEADER_TEXT, - buttonTitle : ORIGIN_BUTTON_TITLE, - type : 'old', - section : 'origin', - isHeader : true, - isOrigin : true, - isSelected : false, - isUnselected: false - } - } - - - handleFailedRequest(vueInstance, data) { - vueInstance.hasError = true; - vueInstance.conflictsData.errorMessage = 'Something went wrong!'; - } - - - getCommitData() { - 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; - } - - - getFilePath(file) { - 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 deleted file mode 100644 index c317a6521b5..00000000000 --- a/app/assets/javascripts/merge_conflict_resolver.js.es6 +++ /dev/null @@ -1,112 +0,0 @@ -//= 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(); - } - - initVue() { - const that = this; - this.vue = new Vue({ - el : '#conflicts', - name : 'MergeConflictResolver', - data : this.dataProvider.getInitialData(), - created : this.fetchData(), - computed : this.setComputedProperties(), - methods : { - 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 - } - }) - } - - - setComputedProperties() { - const dp = this.dataProvider; - - return { - conflictsCount() { return dp.getConflictsCount() }, - readyToCommit() { return dp.isReadyToCommit() }, - commitButtonText() { return dp.getCommitButtonText() } - } - } - - - fetchData() { - const dp = this.dataProvider; - - $.get($('#conflicts').data('conflictsPath')) - .done((data) => { - dp.decorateData(this.vue, data); - }) - .error((data) => { - dp.handleFailedRequest(this.vue, data); - }) - .always(() => { - this.vue.isLoading = false; - - this.vue.$nextTick(() => { - $('#conflicts .js-syntax-highlight').syntaxHighlight(); - }); - - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout); - }) - } - - - commit() { - this.vue.isSubmitting = true; - - $.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); - } -} -- cgit v1.2.1 From 6141816c233d87888d1450d3bc48a28fc6044ec1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 5 Oct 2016 20:49:43 -0500 Subject: Use plain JS to get elements and data and values --- .../javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index 12688365f19..7fd3749b3e2 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -10,14 +10,14 @@ $(() => { const INTERACTIVE_RESOLVE_MODE = 'interactive'; - const $conflicts = $(document.getElementById('conflicts')); + const conflictsEl = document.querySelector('#conflicts'); const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore; const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({ - conflictsPath: $conflicts.data('conflictsPath'), - resolveConflictsPath: $conflicts.data('resolveConflictsPath') + conflictsPath: conflictsEl.dataset.conflictsPath, + resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath }); - gl.MergeConflictsResolverApp = new Vue({ + gl.MergeConflictsResolverApp = new Vue({ el: '#conflicts', data: mergeConflictsStore.state, components: { @@ -48,7 +48,7 @@ $(() => { mergeConflictsStore.setLoadingState(false); this.$nextTick(() => { - $conflicts.find('.js-syntax-highlight').syntaxHighlight(); + $(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight(); }); }); }, -- cgit v1.2.1 From edd30976bd8069a02e0674ee36d1455c06ea751f Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 12 Oct 2016 18:53:29 -0500 Subject: Update CHANGELOG --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 99bbd99726d..f0b250f365c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -8,6 +8,8 @@ v 8.13.0 (unreleased) - Update runner version only when updating contacted_at - Add link from system note to compare with previous version - Use gitlab-shell v3.6.6 + - Ability to resolve merge request conflicts with editor !6374 + - Use gitlab-shell v3.6.2 (GIT TRACE logging) - Add `/projects/visible` API endpoint (Ben Boeckel) - Fix centering of custom header logos (Ashley Dumaine) - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup -- cgit v1.2.1 From cdd0dd507d1d5a226f1f438c4bfa602a115caac3 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 12 Oct 2016 22:52:44 -0500 Subject: Fix discard message --- .../merge_requests/conflicts/components/_diff_file_editor.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 943ae6ee129..3c927d362c2 100644 --- 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 @@ -2,7 +2,7 @@ .diff-editor-wrap{ "v-show" => "file.showEditor" } .discard-changes-alert-wrap{ "v-if" => "file.promptDiscardConfirmation" } .discard-changes-alert - Are you sure to discard your changes? + Are you sure you want to discard your changes? .discard-actions %button.btn.btn-sm.btn-close{ "@click" => "acceptDiscardConfirmation(file)" } Discard changes %button.btn.btn-sm{ "@click" => "cancelDiscardConfirmation(file)" } Cancel -- cgit v1.2.1 From 3764fd4b4166bdf869cc04e4e82558d6b80b4ca5 Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Thu, 13 Oct 2016 10:34:15 +0100 Subject: Add blob_ace_mode to conflict content response --- lib/gitlab/conflict/file.rb | 5 +++++ spec/controllers/projects/merge_requests_controller_spec.rb | 1 + spec/lib/gitlab/conflict/file_spec.rb | 11 +++++++++++ 3 files changed, 17 insertions(+) diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb index 661e43d3fa9..c843315782d 100644 --- a/lib/gitlab/conflict/file.rb +++ b/lib/gitlab/conflict/file.rb @@ -25,6 +25,10 @@ module Gitlab merge_file_result[:data] end + def our_blob + @our_blob ||= repository.blob_at(merge_request.diff_refs.head_sha, our_path) + end + def type lines unless @type @@ -209,6 +213,7 @@ module Gitlab json_hash.tap do |json_hash| if opts[:full_content] json_hash[:content] = content + json_hash[:blob_ace_mode] = our_blob && our_blob.language.try(:ace_mode) else json_hash[:sections] = sections if type.text? json_hash[:type] = type diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 31f43bdc89a..3fe90375b92 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -712,6 +712,7 @@ describe Projects::MergeRequestsController do 'new_path' => path, 'blob_icon' => 'file-text-o', 'blob_path' => a_string_ending_with(path), + 'blob_ace_mode' => 'ruby', 'content' => content) end end diff --git a/spec/lib/gitlab/conflict/file_spec.rb b/spec/lib/gitlab/conflict/file_spec.rb index 60020487061..648d342ecf8 100644 --- a/spec/lib/gitlab/conflict/file_spec.rb +++ b/spec/lib/gitlab/conflict/file_spec.rb @@ -257,5 +257,16 @@ FILE it 'includes the blob icon for the file' do expect(conflict_file.as_json[:blob_icon]).to eq('file-text-o') end + + context 'with the full_content option passed' do + it 'includes the full content of the conflict' do + expect(conflict_file.as_json(full_content: true)).to have_key(:content) + end + + it 'includes the detected language of the conflict file' do + expect(conflict_file.as_json(full_content: true)[:blob_ace_mode]). + to eq('ruby') + end + end end end -- cgit v1.2.1 From f5de2ce3fd74af214556f677f54efa3f4da13ed3 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 13 Oct 2016 12:32:35 -0500 Subject: Add syntax highlighting to files --- .../javascripts/merge_conflicts/components/diff_file_editor.js.es6 | 1 + 1 file changed, 1 insertion(+) 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 index 417095a8db7..3379414343f 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -55,6 +55,7 @@ this.fileLoaded = true; this.editor = ace.edit(content); this.editor.$blockScrolling = Infinity; // Turn off annoying warning + this.editor.getSession().setMode(`ace/mode/${file.blob_ace_mode}`); this.editor.on('change', () => { this.saveDiffResolution(); }); -- cgit v1.2.1 From 26e327ea93140f6400b965b845958ff50461718c Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 13 Oct 2016 14:17:28 -0500 Subject: Update CHANGELOG --- CHANGELOG | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index f0b250f365c..e9f84503b2b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,7 +9,6 @@ v 8.13.0 (unreleased) - Add link from system note to compare with previous version - Use gitlab-shell v3.6.6 - Ability to resolve merge request conflicts with editor !6374 - - Use gitlab-shell v3.6.2 (GIT TRACE logging) - Add `/projects/visible` API endpoint (Ben Boeckel) - Fix centering of custom header logos (Ashley Dumaine) - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup -- cgit v1.2.1 From 317e48193fe5d75d6671c900a765ead719e6b4df Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Wed, 12 Oct 2016 15:34:47 +0200 Subject: Fix the diff in the merge request view when converting a symlink to a regular file. In this specific case using file_path as a cache key is not enough, because there are two entries with the same path. Closes #21610. --- CHANGELOG.md | 1 + lib/gitlab/diff/file.rb | 4 ++++ lib/gitlab/diff/file_collection/merge_request_diff.rb | 10 +++++----- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd37d9bcde6..015d520db0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Reduce queries needed to find users using their SSH keys when pushing commits - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska) - Fix broken repository 500 errors in project list + - Fix the diff in the merge request view when converting a symlink to a regular file - Fix Pipeline list commit column width should be adjusted - Close todos when accepting merge requests via the API !6486 (tonygambone) - Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo) diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index e47df508ca2..ce85e5e0123 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -125,6 +125,10 @@ module Gitlab repository.blob_at(commit.id, file_path) end + + def cache_key + "#{file_path}-#{new_file}-#{deleted_file}-#{renamed_file}" + end end end end diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb index 36348b33943..dc4d47c878b 100644 --- a/lib/gitlab/diff/file_collection/merge_request_diff.rb +++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb @@ -35,16 +35,16 @@ module Gitlab # for the highlighted ones, so we just skip their execution. # If the highlighted diff files lines are not cached we calculate and cache them. # - # The content of the cache is a Hash where the key correspond to the file_path and the values are Arrays of + # The content of the cache is a Hash where the key identifies the file and the values are Arrays of # hashes that represent serialized diff lines. # def cache_highlight!(diff_file) - file_path = diff_file.file_path + item_key = diff_file.cache_key - if highlight_cache[file_path] - highlight_diff_file_from_cache!(diff_file, highlight_cache[file_path]) + if highlight_cache[item_key] + highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key]) else - highlight_cache[file_path] = diff_file.highlighted_diff_lines.map(&:to_hash) + highlight_cache[item_key] = diff_file.highlighted_diff_lines.map(&:to_hash) end end -- cgit v1.2.1 From 501d1485edf321d9af242af4721dec604b8dfbcc Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Fri, 14 Oct 2016 13:55:10 -0700 Subject: Apply better hierarchy to markdown headers and issue/mr titles --- app/assets/stylesheets/framework/typography.scss | 22 ++++++++++------------ app/assets/stylesheets/pages/detail_page.scss | 6 ++++-- app/assets/stylesheets/pages/merge_requests.scss | 7 ------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 287653beac5..8f8ad728269 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -45,40 +45,38 @@ } h1 { - font-size: 2em; + font-size: 1.75em; font-weight: 600; - margin: 1em 0 10px; + margin: 16px 0 10px; padding: 0 0 0.3em; border-bottom: 1px solid $btn-default-border; color: $gl-gray-dark; } h2 { - font-size: 1.6em; + font-size: 1.5em; font-weight: 600; - margin: 1em 0 10px; - padding-bottom: 0.3em; - border-bottom: 1px solid $btn-default-border; + margin: 16px 0 10px; color: $gl-gray-dark; } h3 { - margin: 1em 0 10px; - font-size: 1.4em; + margin: 16px 0 10px; + font-size: 1.3em; } h4 { - margin: 1em 0 10px; - font-size: 1.25em; + margin: 16px 0 10px; + font-size: 1.2em; } h5 { - margin: 1em 0 10px; + margin: 16px 0 10px; font-size: 1em; } h6 { - margin: 1em 0 10px; + margin: 16px 0 10px; font-size: 0.95em; } diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 4d9c73c6840..7aa156bc47a 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -20,9 +20,11 @@ .detail-page-description { .title { - margin: 0; - font-size: 23px; + margin: 0 0 16px; + font-size: 2em; color: $gl-gray-dark; + padding: 0 0 0.3em; + border-bottom: 1px solid #e7e9ed; } .description { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index afc4e517fde..101472278e2 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -429,13 +429,6 @@ } } -.merge-request-details { - - .title { - margin-bottom: 20px; - } -} - .merge-request-tabs { background-color: #fff; -- cgit v1.2.1 From 28dbfd5c4c7793be4147ba217e7d9e162905088b Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 17 Oct 2016 12:43:17 -0500 Subject: Provide better error message to the user --- .../javascripts/merge_conflicts/components/diff_file_editor.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 3379414343f..5012bdfe997 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 @@ -62,7 +62,7 @@ this.saveDiffResolution(); }) .fail(() => { - console.log('error'); + new Flash('Failed to load the file, please try again.'); }) .always(() => { this.loading = false; -- cgit v1.2.1 From 22ef066862bffc443baf8a630b0752ce37dab01c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 14 Oct 2016 11:30:06 +0200 Subject: Avoid race condition when expiring artifacts It may happen that job meant to remove expired artifacts will be executed asynchronously when, in the meantime, project associated with given build gets removed by another asynchronous job. In that case we should not remove artifacts because such build will be removed anyway, when project removal is complete. --- .../expire_build_instance_artifacts_worker.rb | 10 +++-- .../expire_build_instance_artifacts_worker_spec.rb | 44 ++++++++++++++++------ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb index 916c2e633c1..d9e2cc37bb3 100644 --- a/app/workers/expire_build_instance_artifacts_worker.rb +++ b/app/workers/expire_build_instance_artifacts_worker.rb @@ -2,10 +2,14 @@ class ExpireBuildInstanceArtifactsWorker include Sidekiq::Worker def perform(build_id) - build = Ci::Build.with_expired_artifacts.reorder(nil).find_by(id: build_id) - return unless build + build = Ci::Build + .with_expired_artifacts + .reorder(nil) + .find_by(id: build_id) - Rails.logger.info "Removing artifacts build #{build.id}..." + return unless build.try(:project) + + Rails.logger.info "Removing artifacts for build #{build.id}..." build.erase_artifacts! end end diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb index 2b140f2ba28..d202b3de77e 100644 --- a/spec/workers/expire_build_instance_artifacts_worker_spec.rb +++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb @@ -6,28 +6,48 @@ describe ExpireBuildInstanceArtifactsWorker do let(:worker) { described_class.new } describe '#perform' do - before { build } - - subject! { worker.perform(build.id) } + before do + worker.perform(build.id) + end context 'with expired artifacts' do - let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) } + let(:artifacts_expiry) { { artifacts_expire_at: Time.now - 7.days } } - it 'does expire' do - expect(build.reload.artifacts_expired?).to be_truthy - end + context 'when associated project is valid' do + let(:build) do + create(:ci_build, :artifacts, artifacts_expiry) + end - it 'does remove files' do - expect(build.reload.artifacts_file.exists?).to be_falsey + it 'does expire' do + expect(build.reload.artifacts_expired?).to be_truthy + end + + it 'does remove files' do + expect(build.reload.artifacts_file.exists?).to be_falsey + end + + it 'does nullify artifacts_file column' do + expect(build.reload.artifacts_file_identifier).to be_nil + end end - it 'does nullify artifacts_file column' do - expect(build.reload.artifacts_file_identifier).to be_nil + context 'when associated project was removed' do + let(:build) do + create(:ci_build, :artifacts, artifacts_expiry) do |build| + build.project.delete + end + end + + it 'does not remove artifacts' do + expect(build.reload.artifacts_file.exists?).to be_truthy + end end end context 'with not yet expired artifacts' do - let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) } + let(:build) do + create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) + end it 'does not expire' do expect(build.reload.artifacts_expired?).to be_falsey -- cgit v1.2.1 From 81f6e6e7768bed7fb63b1a8516dd0d0daf14f5d9 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Fri, 14 Oct 2016 13:23:02 +0200 Subject: Add Changelog entry to fix for removing artifacts --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd37d9bcde6..45b17a9f219 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.13.0 (2016-10-22) +v 8.13.0 (unreleased) + - Avoid race condition when asynchronously removing expired artifacts. (!6881) - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) - Respond with 404 Not Found for non-existent tags (Linus Thiel) - Truncate long labels with ellipsis in labels page -- cgit v1.2.1 From eb574e4a0ad19a2848abff56a0ce7c44d3f6780e Mon Sep 17 00:00:00 2001 From: tauriedavis Date: Mon, 17 Oct 2016 13:23:28 -0700 Subject: change border color to variable --- app/assets/stylesheets/framework/typography.scss | 6 +++--- app/assets/stylesheets/pages/detail_page.scss | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 8f8ad728269..55de9053be5 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -49,7 +49,7 @@ font-weight: 600; margin: 16px 0 10px; padding: 0 0 0.3em; - border-bottom: 1px solid $btn-default-border; + border-bottom: 1px solid $white-dark; color: $gl-gray-dark; } @@ -85,12 +85,12 @@ font-size: inherit; padding: 8px 21px; margin: 12px 0; - border-left: 3px solid #e7e9ed; + border-left: 3px solid $white-dark; } blockquote:dir(rtl) { border-left: 0; - border-right: 3px solid #e7e9ed; + border-right: 3px solid $white-dark; } blockquote p { diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 7aa156bc47a..2357671c2ae 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -24,7 +24,7 @@ font-size: 2em; color: $gl-gray-dark; padding: 0 0 0.3em; - border-bottom: 1px solid #e7e9ed; + border-bottom: 1px solid $white-dark; } .description { -- cgit v1.2.1 From 73988dd994d6c0d7f5cec24ca5c4c20137cc3844 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 17 Oct 2016 17:47:07 -0500 Subject: Update endpoint to username validator --- app/assets/javascripts/username_validator.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6 index 2517f778365..bf4b2e320cd 100644 --- a/app/assets/javascripts/username_validator.js.es6 +++ b/app/assets/javascripts/username_validator.js.es6 @@ -76,7 +76,7 @@ this.renderState(); return $.ajax({ type: 'GET', - url: `/u/${username}/exists`, + url: `/users/${username}/exists`, dataType: 'json', success: (res) => this.setAvailabilityState(res.exists) }); -- cgit v1.2.1 From 6a16697ad29bc2136411faf2431baa94f2a599e0 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Thu, 13 Oct 2016 13:01:27 +0200 Subject: Execute specific named route method from toggle_award_url helper method --- CHANGELOG.md | 1 + app/helpers/award_emoji_helper.rb | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dbe1832de0..81c25445ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Add Issue Board API support (andrebsguedes) - Allow the Koding integration to be configured through the API - Add new issue button to each list on Issues Board + - Execute specific named route method from toggle_award_url helper method - Added soft wrap button to repository file/blob editor - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms) - Show the time ago a merge request was deployed to an environment diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index aa134cea31c..493f14f6f9d 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -1,9 +1,12 @@ module AwardEmojiHelper def toggle_award_url(awardable) - if @project - url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) + return url_for([:toggle_award_emoji, awardable]) unless @project + + if awardable.is_a?(Note) + # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (6.5x) + toggle_award_emoji_namespace_project_note_url(namespace_id: @project.namespace_id, project_id: @project.id, id: awardable.id) else - url_for([:toggle_award_emoji, awardable]) + url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) end end end -- cgit v1.2.1 From b63b13f930804c83682f6424b8dddf2236649d6d Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Tue, 18 Oct 2016 11:15:44 +0200 Subject: Fix Changelog entry for fix to CI artifacts erase [ci skip] --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b17a9f219..a2b8641d12f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ Please view this file on the master branch, on stable branches it's out of date. ## 8.13.0 (2016-10-22) -v 8.13.0 (unreleased) - Avoid race condition when asynchronously removing expired artifacts. (!6881) - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675) - Respond with 404 Not Found for non-existent tags (Linus Thiel) -- cgit v1.2.1