From 6109daf480327581b6e2dcdfffe90464be6c7796 Mon Sep 17 00:00:00 2001 From: Scott Le <scott.lee318@gmail.com> Date: Thu, 28 Jul 2016 11:04:57 +0700 Subject: [PATCH] api for generating new merge request DRY code + fix rubocop Add more test cases Append to changelog DRY changes list find_url service for merge_requests use GET for getting merge request links remove files rename to get_url_service reduce loop add test case for cross project refactor tiny thing update changelog --- CHANGELOG | 1 + app/models/merge_request.rb | 1 + .../merge_requests/get_urls_service.rb | 52 +++++++++ lib/api/internal.rb | 4 + lib/gitlab/changes_list.rb | 25 +++++ lib/gitlab/checks/change_access.rb | 24 +---- lib/gitlab/git.rb | 18 ++++ lib/gitlab/git_access.rb | 4 +- spec/lib/gitlab/changes_list_spec.rb | 30 ++++++ spec/requests/api/internal_spec.rb | 18 ++++ .../merge_requests/get_urls_service_spec.rb | 100 ++++++++++++++++++ 11 files changed, 254 insertions(+), 23 deletions(-) create mode 100644 app/services/merge_requests/get_urls_service.rb create mode 100644 lib/gitlab/changes_list.rb create mode 100644 spec/lib/gitlab/changes_list_spec.rb create mode 100644 spec/services/merge_requests/get_urls_service_spec.rb diff --git a/CHANGELOG b/CHANGELOG index 282e4b9b449..2f527bde05f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -102,6 +102,7 @@ v 8.11.0 (unreleased) - Fix importing GitLab projects with an invalid MR source project - Sort folders with submodules in Files view !5521 - Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0 + - Print urls to create (or view) merge requests after git push !5542 (Scott Le) v 8.10.5 - Add a data migration to fix some missing timestamps in the members table. !5670 diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b1fb3ce5d69..f6d0d0c98f5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -104,6 +104,7 @@ class MergeRequest < ActiveRecord::Base scope :from_project, ->(project) { where(source_project_id: project.id) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } + scope :from_source_branches, ->(branches) { where(source_branch: branches) } scope :join_project, -> { joins(:target_project) } scope :references_project, -> { references(:target_project) } diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb new file mode 100644 index 00000000000..501fd135e16 --- /dev/null +++ b/app/services/merge_requests/get_urls_service.rb @@ -0,0 +1,52 @@ +module MergeRequests + class GetUrlsService < BaseService + attr_reader :project + + def initialize(project) + @project = project + end + + def execute(changes) + branches = get_branches(changes) + merge_requests_map = opened_merge_requests_from_source_branches(branches) + branches.map do |branch| + existing_merge_request = merge_requests_map[branch] + if existing_merge_request + url_for_existing_merge_request(existing_merge_request) + else + url_for_new_merge_request(branch) + end + end + end + + private + + def opened_merge_requests_from_source_branches(branches) + merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches) + merge_requests.inject({}) do |hash, mr| + hash[mr.source_branch] = mr + hash + end + end + + def get_branches(changes) + changes_list = Gitlab::ChangesList.new(changes) + changes_list.map do |change| + next unless Gitlab::Git.branch_ref?(change[:ref]) + Gitlab::Git.branch_name(change[:ref]) + end.compact + end + + def url_for_new_merge_request(branch_name) + merge_request_params = { source_branch: branch_name } + url = Gitlab::Routing.url_helpers.new_namespace_project_merge_request_url(project.namespace, project, merge_request: merge_request_params) + { branch_name: branch_name, url: url, new_merge_request: true } + end + + def url_for_existing_merge_request(merge_request) + target_project = merge_request.target_project + url = Gitlab::Routing.url_helpers.namespace_project_merge_request_url(target_project.namespace, target_project, merge_request) + { branch_name: merge_request.source_branch, url: url, new_merge_request: false } + end + end +end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 959b700de78..d8e9ac406c4 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -74,6 +74,10 @@ module API response end + get "/merge_request_urls" do + ::MergeRequests::GetUrlsService.new(project).execute(params[:changes]) + end + # # Discover user by ssh key # diff --git a/lib/gitlab/changes_list.rb b/lib/gitlab/changes_list.rb new file mode 100644 index 00000000000..95308aca95f --- /dev/null +++ b/lib/gitlab/changes_list.rb @@ -0,0 +1,25 @@ +module Gitlab + class ChangesList + include Enumerable + + attr_reader :raw_changes + + def initialize(changes) + @raw_changes = changes.kind_of?(String) ? changes.lines : changes + end + + def each(&block) + changes.each(&block) + end + + def changes + @changes ||= begin + @raw_changes.map do |change| + next if change.blank? + oldrev, newrev, ref = change.strip.split(' ') + { oldrev: oldrev, newrev: newrev, ref: ref } + end.compact + end + end + end +end diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 5551fac4b8b..52f117e963b 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -4,8 +4,8 @@ module Gitlab attr_reader :user_access, :project def initialize(change, user_access:, project:) - @oldrev, @newrev, @ref = change.split(' ') - @branch_name = branch_name(@ref) + @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) + @branch_name = Gitlab::Git.branch_name(@ref) @user_access = user_access @project = project end @@ -47,7 +47,7 @@ module Gitlab end def tag_checks - tag_ref = tag_name(@ref) + tag_ref = Gitlab::Git.tag_name(@ref) if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project) "You are not allowed to change existing tags on this project." @@ -73,24 +73,6 @@ module Gitlab def matching_merge_request? Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match? end - - def branch_name(ref) - ref = @ref.to_s - if Gitlab::Git.branch_ref?(ref) - Gitlab::Git.ref_name(ref) - else - nil - end - end - - def tag_name(ref) - ref = @ref.to_s - if Gitlab::Git.tag_ref?(ref) - Gitlab::Git.ref_name(ref) - else - nil - end - end end end end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 191bea86ac3..7584efe4fa8 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -9,6 +9,24 @@ module Gitlab ref.gsub(/\Arefs\/(tags|heads)\//, '') end + def branch_name(ref) + ref = ref.to_s + if self.branch_ref?(ref) + self.ref_name(ref) + else + nil + end + end + + def tag_name(ref) + ref = ref.to_s + if self.tag_ref?(ref) + self.ref_name(ref) + else + nil + end + end + def tag_ref?(ref) ref.start_with?(TAG_REF_PREFIX) end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 5bc70f530d2..1882eb8d050 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -76,10 +76,10 @@ module Gitlab return build_status_object(false, "A repository for this project does not exist yet.") end - changes = changes.lines if changes.kind_of?(String) + changes_list = Gitlab::ChangesList.new(changes) # Iterate over all changes to find if user allowed all of them to be applied - changes.map(&:strip).reject(&:blank?).each do |change| + changes_list.each do |change| status = change_access_check(change) unless status.allowed? # If user does not have access to make at least one change - cancel all push diff --git a/spec/lib/gitlab/changes_list_spec.rb b/spec/lib/gitlab/changes_list_spec.rb new file mode 100644 index 00000000000..69d86144e32 --- /dev/null +++ b/spec/lib/gitlab/changes_list_spec.rb @@ -0,0 +1,30 @@ +require "spec_helper" + +describe Gitlab::ChangesList do + let(:valid_changes_string) { "\n000000 570e7b2 refs/heads/my_branch\nd14d6c 6fd24d refs/heads/master" } + let(:invalid_changes) { 1 } + + context 'when changes is a valid string' do + let(:changes_list) { Gitlab::ChangesList.new(valid_changes_string) } + + it 'splits elements by newline character' do + expect(changes_list).to contain_exactly({ + oldrev: "000000", + newrev: "570e7b2", + ref: "refs/heads/my_branch" + }, { + oldrev: "d14d6c", + newrev: "6fd24d", + ref: "refs/heads/master" + }) + end + + it 'behaves like a list' do + expect(changes_list.first).to eq({ + oldrev: "000000", + newrev: "570e7b2", + ref: "refs/heads/my_branch" + }) + end + end +end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index f6f85d6e95e..be52f88831f 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -275,6 +275,24 @@ describe API::API, api: true do end end + describe 'GET /internal/merge_request_urls' do + let(:repo_name) { "#{project.namespace.name}/#{project.path}" } + let(:changes) { URI.escape("#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch") } + + before do + project.team << [user, :developer] + get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token + end + + it 'returns link to create new merge request' do + expect(json_response).to match [{ + "branch_name" => "new_branch", + "url" => "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch", + "new_merge_request" => true + }] + end + end + def pull(key, project, protocol = 'ssh') post( api("/internal/allowed"), diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb new file mode 100644 index 00000000000..ec26770c3eb --- /dev/null +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -0,0 +1,100 @@ +require "spec_helper" + +describe MergeRequests::GetUrlsService do + let(:project) { create(:project, :public) } + let(:service) { MergeRequests::GetUrlsService.new(project) } + let(:source_branch) { "my_branch" } + let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } + let(:show_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } + let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } + + describe "#execute" do + shared_examples 'new_merge_request_link' do + it 'returns url to create new merge request' do + result = service.execute(changes) + expect(result).to match([{ + branch_name: source_branch, + url: new_merge_request_url, + new_merge_request: true + }]) + end + end + + shared_examples 'show_merge_request_url' do + it 'returns url to view merge request' do + result = service.execute(changes) + expect(result).to match([{ + branch_name: source_branch, + url: show_merge_request_url, + new_merge_request: false + }]) + end + end + + context 'pushing one completely new branch' do + let(:changes) { new_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing to existing branch but no merge request' do + let(:changes) { existing_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing to existing branch and merge request opened' do + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'show_merge_request_url' + end + + context 'pushing to existing branch and merge request is reopened' do + let!(:merge_request) { create(:merge_request, :reopened, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'show_merge_request_url' + end + + context 'pushing to existing branch from forked project' do + let(:user) { create(:user) } + let!(:forked_project) { Projects::ForkService.new(project, user).execute } + let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + # Source project is now the forked one + let(:service) { MergeRequests::GetUrlsService.new(forked_project) } + it_behaves_like 'show_merge_request_url' + end + + context 'pushing to existing branch and merge request is closed' do + let!(:merge_request) { create(:merge_request, :closed, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing to existing branch and merge request is merged' do + let!(:merge_request) { create(:merge_request, :merged, source_project: project, source_branch: source_branch) } + let(:changes) { existing_branch_changes } + it_behaves_like 'new_merge_request_link' + end + + context 'pushing new branch and existing branch (with merge request created) at once' do + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") } + let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" } + let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } + let(:new_merge_request_url) { "http://localhost/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } + + it 'returns 2 urls for both creating new and showing merge request' do + result = service.execute(changes) + expect(result).to match([{ + branch_name: "new_branch", + url: new_merge_request_url, + new_merge_request: true + }, { + branch_name: "existing_branch", + url: show_merge_request_url, + new_merge_request: false + }]) + end + end + end +end -- GitLab