2012-11-24 18:04:13 -05:00
|
|
|
require 'spec_helper'
|
|
|
|
|
2013-06-23 13:25:06 -04:00
|
|
|
describe Projects::MergeRequestsController do
|
2017-09-29 04:04:50 -04:00
|
|
|
include ProjectForksHelper
|
|
|
|
|
2017-08-01 14:51:52 -04:00
|
|
|
let(:project) { create(:project, :repository) }
|
Use CTEs for nested groups and authorizations
This commit introduces the usage of Common Table Expressions (CTEs) to
efficiently retrieve nested group hierarchies, without having to rely on
the "routes" table (which is an _incredibly_ inefficient way of getting
the data). This requires a patch to ActiveRecord (found in the added
initializer) to work properly as ActiveRecord doesn't support WITH
statements properly out of the box.
Unfortunately MySQL provides no efficient way of getting nested groups.
For example, the old routes setup could easily take 5-10 seconds
depending on the amount of "routes" in a database. Providing vastly
different logic for both MySQL and PostgreSQL will negatively impact the
development process. Because of this the various nested groups related
methods return empty relations when used in combination with MySQL.
For project authorizations the logic is split up into two classes:
* Gitlab::ProjectAuthorizations::WithNestedGroups
* Gitlab::ProjectAuthorizations::WithoutNestedGroups
Both classes get the fresh project authorizations (= as they should be
in the "project_authorizations" table), including nested groups if
PostgreSQL is used. The logic of these two classes is quite different
apart from their public interface. This complicates development a bit,
but unfortunately there is no way around this.
This commit also introduces Gitlab::GroupHierarchy. This class can be
used to get the ancestors and descendants of a base relation, or both by
using a UNION. This in turn is used by methods such as:
* Namespace#ancestors
* Namespace#descendants
* User#all_expanded_groups
Again this class relies on CTEs and thus only works on PostgreSQL. The
Namespace methods will return an empty relation when MySQL is used,
while User#all_expanded_groups will return only the groups a user is a
direct member of.
Performance wise the impact is quite large. For example, on GitLab.com
Namespace#descendants used to take around 580 ms to retrieve data for a
particular user. Using CTEs we are able to reduce this down to roughly 1
millisecond, returning the exact same data.
== On The Fly Refreshing
Refreshing of authorizations on the fly (= when
users.authorized_projects_populated was not set) is removed with this
commit. This simplifies the code, and ensures any queries used for
authorizations are not mutated because they are executed in a Rails
scope (e.g. Project.visible_to_user).
This commit includes a migration to schedule refreshing authorizations
for all users, ensuring all of them have their authorizations in place.
Said migration schedules users in batches of 5000, with 5 minutes
between every batch to smear the load around a bit.
== Spec Changes
This commit also introduces some changes to various specs. For example,
some specs for ProjectTeam assumed that creating a personal project
would _not_ lead to the owner having access, which is incorrect. Because
we also no longer refresh authorizations on the fly for new users some
code had to be added to the "empty_project" factory. This chunk of code
ensures that the owner's permissions are refreshed after creating the
project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
|
|
|
let(:user) { project.owner }
|
2014-08-06 02:07:11 -04:00
|
|
|
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
|
2016-07-27 12:54:04 -04:00
|
|
|
let(:merge_request_with_conflicts) do
|
2016-08-05 07:15:06 -04:00
|
|
|
create(:merge_request, source_branch: 'conflict-resolvable', target_branch: 'conflict-start', source_project: project) do |mr|
|
2016-07-27 12:54:04 -04:00
|
|
|
mr.mark_as_unmergeable
|
|
|
|
end
|
|
|
|
end
|
2012-11-24 18:04:13 -05:00
|
|
|
|
|
|
|
before do
|
|
|
|
sign_in(user)
|
|
|
|
end
|
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
describe 'GET commit_change_content' do
|
|
|
|
it 'renders commit_change_content template' do
|
|
|
|
get :commit_change_content,
|
|
|
|
namespace_id: project.namespace.to_param,
|
|
|
|
project_id: project,
|
|
|
|
id: merge_request.iid,
|
|
|
|
format: 'html'
|
|
|
|
|
|
|
|
expect(response).to render_template('_commit_change_content')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-11-10 19:04:28 -05:00
|
|
|
shared_examples "loads labels" do |action|
|
|
|
|
it "loads labels into the @labels variable" do
|
|
|
|
get action,
|
|
|
|
namespace_id: project.namespace.to_param,
|
2017-02-23 18:55:01 -05:00
|
|
|
project_id: project,
|
2016-11-10 19:04:28 -05:00
|
|
|
id: merge_request.iid,
|
|
|
|
format: 'html'
|
|
|
|
expect(assigns(:labels)).not_to be_nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-08 13:11:47 -04:00
|
|
|
describe "GET show" do
|
2017-05-09 00:15:34 -04:00
|
|
|
def go(extra_params = {})
|
|
|
|
params = {
|
|
|
|
namespace_id: project.namespace.to_param,
|
|
|
|
project_id: project,
|
|
|
|
id: merge_request.iid
|
|
|
|
}
|
2012-11-24 18:04:13 -05:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
get :show, params.merge(extra_params)
|
|
|
|
end
|
2012-11-24 18:04:13 -05:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
it_behaves_like "loads labels", :show
|
2016-11-10 19:04:28 -05:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
describe 'as html' do
|
|
|
|
it "renders merge request page" do
|
|
|
|
go(format: :html)
|
2012-11-24 18:04:13 -05:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(response).to be_success
|
2012-11-24 18:04:13 -05:00
|
|
|
end
|
2017-08-29 09:46:40 -04:00
|
|
|
|
|
|
|
context "loads notes" do
|
|
|
|
let(:first_contributor) { create(:user) }
|
|
|
|
let(:contributor) { create(:user) }
|
|
|
|
let(:merge_request) { create(:merge_request, author: first_contributor, target_project: project, source_project: project) }
|
|
|
|
let(:contributor_merge_request) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) }
|
|
|
|
# the order here is important
|
|
|
|
# as the controller reloads these from DB, references doesn't correspond after
|
|
|
|
let!(:first_contributor_note) { create(:note, author: first_contributor, noteable: merge_request, project: project) }
|
|
|
|
let!(:contributor_note) { create(:note, author: contributor, noteable: merge_request, project: project) }
|
|
|
|
let!(:owner_note) { create(:note, author: user, noteable: merge_request, project: project) }
|
|
|
|
|
|
|
|
it "with special_role FIRST_TIME_CONTRIBUTOR" do
|
|
|
|
go(format: :html)
|
|
|
|
|
|
|
|
notes = assigns(:notes)
|
|
|
|
expect(notes).to match(a_collection_containing_exactly(an_object_having_attributes(special_role: Note::SpecialRole::FIRST_TIME_CONTRIBUTOR),
|
|
|
|
an_object_having_attributes(special_role: nil),
|
|
|
|
an_object_having_attributes(special_role: nil)
|
|
|
|
))
|
|
|
|
end
|
|
|
|
end
|
2017-05-09 00:15:34 -04:00
|
|
|
end
|
2012-11-24 18:04:13 -05:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
describe 'as json' do
|
|
|
|
context 'with basic param' do
|
|
|
|
it 'renders basic MR entity as json' do
|
|
|
|
go(basic: true, format: :json)
|
2012-11-24 18:04:13 -05:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(response).to match_response_schema('entities/merge_request_basic')
|
|
|
|
end
|
2012-11-24 18:04:13 -05:00
|
|
|
end
|
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
context 'without basic param' do
|
|
|
|
it 'renders the merge request in the json format' do
|
|
|
|
go(format: :json)
|
2012-11-24 18:04:13 -05:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(response).to match_response_schema('entities/merge_request')
|
|
|
|
end
|
2012-11-24 18:04:13 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "as diff" do
|
2016-05-12 14:50:49 -04:00
|
|
|
it "triggers workhorse to serve the request" do
|
2017-05-09 00:15:34 -04:00
|
|
|
go(format: :diff)
|
2012-11-24 18:04:13 -05:00
|
|
|
|
2016-06-08 08:30:15 -04:00
|
|
|
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
|
2012-11-24 18:04:13 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "as patch" do
|
2016-06-10 08:57:50 -04:00
|
|
|
it 'triggers workhorse to serve the request' do
|
2017-05-09 00:15:34 -04:00
|
|
|
go(format: :patch)
|
2012-11-24 18:04:13 -05:00
|
|
|
|
2016-07-03 17:01:13 -04:00
|
|
|
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
|
2012-11-24 18:04:13 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2015-04-13 01:27:45 -04:00
|
|
|
|
2016-07-08 13:11:47 -04:00
|
|
|
describe 'GET index' do
|
2017-08-01 12:55:57 -04:00
|
|
|
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
|
2017-01-23 15:40:25 -05:00
|
|
|
|
2016-12-20 13:52:09 -05:00
|
|
|
def get_merge_requests(page = nil)
|
2016-02-17 21:15:13 -05:00
|
|
|
get :index,
|
|
|
|
namespace_id: project.namespace.to_param,
|
2017-02-23 18:55:01 -05:00
|
|
|
project_id: project,
|
2016-12-20 13:52:09 -05:00
|
|
|
state: 'opened', page: page.to_param
|
|
|
|
end
|
|
|
|
|
2017-01-23 15:40:25 -05:00
|
|
|
it_behaves_like "issuables list meta-data", :merge_request
|
|
|
|
|
2016-12-20 13:52:09 -05:00
|
|
|
context 'when page param' do
|
2016-12-21 10:02:05 -05:00
|
|
|
let(:last_page) { project.merge_requests.page().total_pages }
|
|
|
|
let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
|
|
|
|
|
2016-12-20 13:52:09 -05:00
|
|
|
it 'redirects to last_page if page number is larger than number of pages' do
|
|
|
|
get_merge_requests(last_page + 1)
|
|
|
|
|
2016-12-21 10:02:05 -05:00
|
|
|
expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
|
2016-12-20 13:52:09 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'redirects to specified page' do
|
|
|
|
get_merge_requests(last_page)
|
|
|
|
|
|
|
|
expect(assigns(:merge_requests).current_page).to eq(last_page)
|
|
|
|
expect(response).to have_http_status(200)
|
|
|
|
end
|
2017-04-05 18:52:19 -04:00
|
|
|
|
|
|
|
it 'does not redirect to external sites when provided a host field' do
|
|
|
|
external_host = "www.example.com"
|
|
|
|
get :index,
|
|
|
|
namespace_id: project.namespace.to_param,
|
|
|
|
project_id: project,
|
|
|
|
state: 'opened',
|
|
|
|
page: (last_page + 1).to_param,
|
|
|
|
host: external_host
|
|
|
|
|
|
|
|
expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
|
|
|
|
end
|
2016-02-17 21:15:13 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
context 'when filtering by opened state' do
|
|
|
|
context 'with opened merge requests' do
|
2016-07-25 14:16:19 -04:00
|
|
|
it 'lists those merge requests' do
|
2017-08-01 12:55:57 -04:00
|
|
|
expect(merge_request).to be_persisted
|
|
|
|
|
2016-02-17 21:15:13 -05:00
|
|
|
get_merge_requests
|
|
|
|
|
|
|
|
expect(assigns(:merge_requests)).to include(merge_request)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with reopened merge requests' do
|
|
|
|
before do
|
|
|
|
merge_request.close!
|
|
|
|
merge_request.reopen!
|
|
|
|
end
|
|
|
|
|
2016-07-25 14:16:19 -04:00
|
|
|
it 'lists those merge requests' do
|
2016-02-17 21:15:13 -05:00
|
|
|
get_merge_requests
|
|
|
|
|
|
|
|
expect(assigns(:merge_requests)).to include(merge_request)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-08 13:11:47 -04:00
|
|
|
describe 'PUT update' do
|
2017-03-18 00:23:15 -04:00
|
|
|
context 'changing the assignee' do
|
|
|
|
it 'limits the attributes exposed on the assignee' do
|
|
|
|
assignee = create(:user)
|
|
|
|
project.add_developer(assignee)
|
|
|
|
|
|
|
|
put :update,
|
|
|
|
namespace_id: project.namespace.to_param,
|
|
|
|
project_id: project,
|
|
|
|
id: merge_request.iid,
|
|
|
|
merge_request: { assignee_id: assignee.id },
|
|
|
|
format: :json
|
|
|
|
body = JSON.parse(response.body)
|
|
|
|
|
|
|
|
expect(body['assignee'].keys)
|
|
|
|
.to match_array(%w(name username avatar_url))
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-04-11 15:21:32 -04:00
|
|
|
context 'there is no source project' do
|
2017-08-01 14:51:52 -04:00
|
|
|
let(:project) { create(:project, :repository) }
|
2017-09-29 04:04:50 -04:00
|
|
|
let(:forked_project) { fork_project_with_submodules(project) }
|
|
|
|
let!(:merge_request) { create(:merge_request, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
|
2016-04-11 15:21:32 -04:00
|
|
|
|
|
|
|
before do
|
2017-09-29 04:04:50 -04:00
|
|
|
forked_project.destroy
|
2016-04-11 15:21:32 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'closes MR without errors' do
|
|
|
|
post :update,
|
2017-02-23 18:55:01 -05:00
|
|
|
namespace_id: project.namespace,
|
|
|
|
project_id: project,
|
2016-04-11 15:21:32 -04:00
|
|
|
id: merge_request.iid,
|
|
|
|
merge_request: {
|
|
|
|
state_event: 'close'
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
|
|
|
|
expect(merge_request.reload.closed?).to be_truthy
|
|
|
|
end
|
2016-07-26 07:57:43 -04:00
|
|
|
|
2016-08-10 09:36:30 -04:00
|
|
|
it 'allows editing of a closed merge request' do
|
2016-07-26 07:57:43 -04:00
|
|
|
merge_request.close!
|
|
|
|
|
|
|
|
put :update,
|
2017-02-23 18:55:01 -05:00
|
|
|
namespace_id: project.namespace,
|
|
|
|
project_id: project,
|
2016-07-26 07:57:43 -04:00
|
|
|
id: merge_request.iid,
|
|
|
|
merge_request: {
|
|
|
|
title: 'New title'
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
|
|
|
|
expect(merge_request.reload.title).to eq 'New title'
|
|
|
|
end
|
|
|
|
|
2016-08-10 09:36:30 -04:00
|
|
|
it 'does not allow to update target branch closed merge request' do
|
2016-07-26 07:57:43 -04:00
|
|
|
merge_request.close!
|
|
|
|
|
|
|
|
put :update,
|
2017-02-23 18:55:01 -05:00
|
|
|
namespace_id: project.namespace,
|
|
|
|
project_id: project,
|
2016-07-26 07:57:43 -04:00
|
|
|
id: merge_request.iid,
|
|
|
|
merge_request: {
|
|
|
|
target_branch: 'new_branch'
|
|
|
|
}
|
|
|
|
|
|
|
|
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
|
|
|
|
end
|
2017-01-16 13:43:03 -05:00
|
|
|
|
|
|
|
it_behaves_like 'update invalid issuable', MergeRequest
|
2016-04-11 15:21:32 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-08 13:11:47 -04:00
|
|
|
describe 'POST merge' do
|
2016-06-01 08:25:44 -04:00
|
|
|
let(:base_params) do
|
|
|
|
{
|
2017-02-23 18:55:01 -05:00
|
|
|
namespace_id: project.namespace,
|
|
|
|
project_id: project,
|
2016-06-01 08:25:44 -04:00
|
|
|
id: merge_request.iid,
|
2017-05-09 00:15:34 -04:00
|
|
|
format: 'json'
|
2016-06-01 08:25:44 -04:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
context 'when user cannot access' do
|
Use CTEs for nested groups and authorizations
This commit introduces the usage of Common Table Expressions (CTEs) to
efficiently retrieve nested group hierarchies, without having to rely on
the "routes" table (which is an _incredibly_ inefficient way of getting
the data). This requires a patch to ActiveRecord (found in the added
initializer) to work properly as ActiveRecord doesn't support WITH
statements properly out of the box.
Unfortunately MySQL provides no efficient way of getting nested groups.
For example, the old routes setup could easily take 5-10 seconds
depending on the amount of "routes" in a database. Providing vastly
different logic for both MySQL and PostgreSQL will negatively impact the
development process. Because of this the various nested groups related
methods return empty relations when used in combination with MySQL.
For project authorizations the logic is split up into two classes:
* Gitlab::ProjectAuthorizations::WithNestedGroups
* Gitlab::ProjectAuthorizations::WithoutNestedGroups
Both classes get the fresh project authorizations (= as they should be
in the "project_authorizations" table), including nested groups if
PostgreSQL is used. The logic of these two classes is quite different
apart from their public interface. This complicates development a bit,
but unfortunately there is no way around this.
This commit also introduces Gitlab::GroupHierarchy. This class can be
used to get the ancestors and descendants of a base relation, or both by
using a UNION. This in turn is used by methods such as:
* Namespace#ancestors
* Namespace#descendants
* User#all_expanded_groups
Again this class relies on CTEs and thus only works on PostgreSQL. The
Namespace methods will return an empty relation when MySQL is used,
while User#all_expanded_groups will return only the groups a user is a
direct member of.
Performance wise the impact is quite large. For example, on GitLab.com
Namespace#descendants used to take around 580 ms to retrieve data for a
particular user. Using CTEs we are able to reduce this down to roughly 1
millisecond, returning the exact same data.
== On The Fly Refreshing
Refreshing of authorizations on the fly (= when
users.authorized_projects_populated was not set) is removed with this
commit. This simplifies the code, and ensures any queries used for
authorizations are not mutated because they are executed in a Rails
scope (e.g. Project.visible_to_user).
This commit includes a migration to schedule refreshing authorizations
for all users, ensuring all of them have their authorizations in place.
Said migration schedules users in batches of 5000, with 5 minutes
between every batch to smear the load around a bit.
== Spec Changes
This commit also introduces some changes to various specs. For example,
some specs for ProjectTeam assumed that creating a personal project
would _not_ lead to the owner having access, which is incorrect. Because
we also no longer refresh authorizations on the fly for new users some
code had to be added to the "empty_project" factory. This chunk of code
ensures that the owner's permissions are refreshed after creating the
project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
|
|
|
let(:user) { create(:user) }
|
|
|
|
|
2016-06-01 08:25:44 -04:00
|
|
|
before do
|
2017-05-09 00:15:34 -04:00
|
|
|
project.add_reporter(user)
|
|
|
|
xhr :post, :merge, base_params
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
it 'returns 404' do
|
|
|
|
expect(response).to have_http_status(404)
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the merge request is not mergeable' do
|
|
|
|
before do
|
|
|
|
merge_request.update_attributes(title: "WIP: #{merge_request.title}")
|
|
|
|
|
|
|
|
post :merge, base_params
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns :failed' do
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(json_response).to eq('status' => 'failed')
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the sha parameter does not match the source SHA' do
|
2017-06-14 14:18:56 -04:00
|
|
|
before do
|
|
|
|
post :merge, base_params.merge(sha: 'foo')
|
|
|
|
end
|
2016-06-01 08:25:44 -04:00
|
|
|
|
|
|
|
it 'returns :sha_mismatch' do
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(json_response).to eq('status' => 'sha_mismatch')
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'when the sha parameter matches the source SHA' do
|
|
|
|
def merge_with_sha
|
2016-06-20 12:48:04 -04:00
|
|
|
post :merge, base_params.merge(sha: merge_request.diff_head_sha)
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns :success' do
|
|
|
|
merge_with_sha
|
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(json_response).to eq('status' => 'success')
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
it 'starts the merge immediately' do
|
|
|
|
expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
|
|
|
|
|
|
|
|
merge_with_sha
|
|
|
|
end
|
|
|
|
|
2017-02-17 08:56:13 -05:00
|
|
|
context 'when the pipeline succeeds is passed' do
|
|
|
|
def merge_when_pipeline_succeeds
|
|
|
|
post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
2017-05-19 16:51:07 -04:00
|
|
|
create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
|
2017-02-17 08:56:13 -05:00
|
|
|
it 'returns :merge_when_pipeline_succeeds' do
|
|
|
|
merge_when_pipeline_succeeds
|
2016-06-01 08:25:44 -04:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
|
2017-02-17 08:56:13 -05:00
|
|
|
it 'sets the MR to merge when the pipeline succeeds' do
|
|
|
|
service = double(:merge_when_pipeline_succeeds_service)
|
2016-06-01 08:25:44 -04:00
|
|
|
|
2017-02-22 17:54:59 -05:00
|
|
|
expect(MergeRequests::MergeWhenPipelineSucceedsService)
|
|
|
|
.to receive(:new).with(project, anything, anything)
|
|
|
|
.and_return(service)
|
2016-06-01 08:25:44 -04:00
|
|
|
expect(service).to receive(:execute).with(merge_request)
|
|
|
|
|
2017-02-17 08:56:13 -05:00
|
|
|
merge_when_pipeline_succeeds
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
2016-06-24 12:13:56 -04:00
|
|
|
|
2017-02-17 08:56:13 -05:00
|
|
|
context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do
|
2016-06-24 12:13:56 -04:00
|
|
|
before do
|
2017-02-17 08:56:13 -05:00
|
|
|
project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
|
2016-06-24 12:13:56 -04:00
|
|
|
end
|
|
|
|
|
2017-02-17 08:56:13 -05:00
|
|
|
it 'returns :merge_when_pipeline_succeeds' do
|
|
|
|
merge_when_pipeline_succeeds
|
2016-06-24 12:13:56 -04:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
|
2016-06-24 12:13:56 -04:00
|
|
|
end
|
|
|
|
end
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
2016-09-16 07:02:42 -04:00
|
|
|
|
2016-10-26 13:19:17 -04:00
|
|
|
describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do
|
|
|
|
let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
|
|
|
|
|
|
|
|
context 'when enabled' do
|
|
|
|
before do
|
|
|
|
project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with unresolved discussion' do
|
|
|
|
before do
|
|
|
|
expect(merge_request).not_to be_discussions_resolved
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns :failed' do
|
|
|
|
merge_with_sha
|
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(json_response).to eq('status' => 'failed')
|
2016-10-26 13:19:17 -04:00
|
|
|
end
|
|
|
|
end
|
2016-09-16 07:02:42 -04:00
|
|
|
|
2016-10-26 13:19:17 -04:00
|
|
|
context 'with all discussions resolved' do
|
|
|
|
before do
|
|
|
|
merge_request.discussions.each { |d| d.resolve!(user) }
|
|
|
|
expect(merge_request).to be_discussions_resolved
|
|
|
|
end
|
2016-09-16 07:02:42 -04:00
|
|
|
|
2016-10-26 13:19:17 -04:00
|
|
|
it 'returns :success' do
|
|
|
|
merge_with_sha
|
2016-09-16 07:02:42 -04:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(json_response).to eq('status' => 'success')
|
2016-10-26 13:19:17 -04:00
|
|
|
end
|
2016-09-16 07:02:42 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-10-26 13:19:17 -04:00
|
|
|
context 'when disabled' do
|
|
|
|
before do
|
|
|
|
project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with unresolved discussion' do
|
|
|
|
before do
|
|
|
|
expect(merge_request).not_to be_discussions_resolved
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'returns :success' do
|
|
|
|
merge_with_sha
|
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(json_response).to eq('status' => 'success')
|
2016-10-26 13:19:17 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
context 'with all discussions resolved' do
|
|
|
|
before do
|
|
|
|
merge_request.discussions.each { |d| d.resolve!(user) }
|
|
|
|
expect(merge_request).to be_discussions_resolved
|
|
|
|
end
|
2016-09-16 07:02:42 -04:00
|
|
|
|
2016-10-26 13:19:17 -04:00
|
|
|
it 'returns :success' do
|
|
|
|
merge_with_sha
|
2016-09-16 07:02:42 -04:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
expect(json_response).to eq('status' => 'success')
|
2016-10-26 13:19:17 -04:00
|
|
|
end
|
2016-09-16 07:02:42 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2016-06-01 08:25:44 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-08 13:11:47 -04:00
|
|
|
describe "DELETE destroy" do
|
Use CTEs for nested groups and authorizations
This commit introduces the usage of Common Table Expressions (CTEs) to
efficiently retrieve nested group hierarchies, without having to rely on
the "routes" table (which is an _incredibly_ inefficient way of getting
the data). This requires a patch to ActiveRecord (found in the added
initializer) to work properly as ActiveRecord doesn't support WITH
statements properly out of the box.
Unfortunately MySQL provides no efficient way of getting nested groups.
For example, the old routes setup could easily take 5-10 seconds
depending on the amount of "routes" in a database. Providing vastly
different logic for both MySQL and PostgreSQL will negatively impact the
development process. Because of this the various nested groups related
methods return empty relations when used in combination with MySQL.
For project authorizations the logic is split up into two classes:
* Gitlab::ProjectAuthorizations::WithNestedGroups
* Gitlab::ProjectAuthorizations::WithoutNestedGroups
Both classes get the fresh project authorizations (= as they should be
in the "project_authorizations" table), including nested groups if
PostgreSQL is used. The logic of these two classes is quite different
apart from their public interface. This complicates development a bit,
but unfortunately there is no way around this.
This commit also introduces Gitlab::GroupHierarchy. This class can be
used to get the ancestors and descendants of a base relation, or both by
using a UNION. This in turn is used by methods such as:
* Namespace#ancestors
* Namespace#descendants
* User#all_expanded_groups
Again this class relies on CTEs and thus only works on PostgreSQL. The
Namespace methods will return an empty relation when MySQL is used,
while User#all_expanded_groups will return only the groups a user is a
direct member of.
Performance wise the impact is quite large. For example, on GitLab.com
Namespace#descendants used to take around 580 ms to retrieve data for a
particular user. Using CTEs we are able to reduce this down to roughly 1
millisecond, returning the exact same data.
== On The Fly Refreshing
Refreshing of authorizations on the fly (= when
users.authorized_projects_populated was not set) is removed with this
commit. This simplifies the code, and ensures any queries used for
authorizations are not mutated because they are executed in a Rails
scope (e.g. Project.visible_to_user).
This commit includes a migration to schedule refreshing authorizations
for all users, ensuring all of them have their authorizations in place.
Said migration schedules users in batches of 5000, with 5 minutes
between every batch to smear the load around a bit.
== Spec Changes
This commit also introduces some changes to various specs. For example,
some specs for ProjectTeam assumed that creating a personal project
would _not_ lead to the owner having access, which is incorrect. Because
we also no longer refresh authorizations on the fly for new users some
code had to be added to the "empty_project" factory. This chunk of code
ensures that the owner's permissions are refreshed after creating the
project, something that is normally done in Projects::CreateService.
2017-04-24 11:19:22 -04:00
|
|
|
let(:user) { create(:user) }
|
|
|
|
|
2016-03-21 09:12:52 -04:00
|
|
|
it "denies access to users unless they're admin or project owner" do
|
2017-02-23 18:55:01 -05:00
|
|
|
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
|
2016-02-26 03:55:43 -05:00
|
|
|
|
2016-06-27 14:10:42 -04:00
|
|
|
expect(response).to have_http_status(404)
|
2016-02-26 03:55:43 -05:00
|
|
|
end
|
|
|
|
|
2016-03-21 09:12:52 -04:00
|
|
|
context "when the user is owner" do
|
|
|
|
let(:owner) { create(:user) }
|
|
|
|
let(:namespace) { create(:namespace, owner: owner) }
|
2017-08-01 14:51:52 -04:00
|
|
|
let(:project) { create(:project, :repository, namespace: namespace) }
|
2016-03-21 09:12:52 -04:00
|
|
|
|
2017-06-14 14:18:56 -04:00
|
|
|
before do
|
|
|
|
sign_in owner
|
|
|
|
end
|
2016-02-26 03:55:43 -05:00
|
|
|
|
2016-03-21 09:12:52 -04:00
|
|
|
it "deletes the merge request" do
|
2017-02-23 18:55:01 -05:00
|
|
|
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
|
2016-02-26 03:55:43 -05:00
|
|
|
|
2016-06-27 14:10:42 -04:00
|
|
|
expect(response).to have_http_status(302)
|
2017-07-24 08:21:16 -04:00
|
|
|
expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./)
|
2016-02-26 03:55:43 -05:00
|
|
|
end
|
2016-09-01 18:12:05 -04:00
|
|
|
|
|
|
|
it 'delegates the update of the todos count cache to TodoService' do
|
|
|
|
expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once
|
|
|
|
|
2017-02-23 18:55:01 -05:00
|
|
|
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
|
2016-09-01 18:12:05 -04:00
|
|
|
end
|
2016-02-26 03:55:43 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-07-08 17:50:06 -04:00
|
|
|
describe 'GET commits' do
|
2015-06-17 16:12:28 -04:00
|
|
|
def go(format: 'html')
|
2015-06-23 01:24:39 -04:00
|
|
|
get :commits,
|
|
|
|
namespace_id: project.namespace.to_param,
|
2017-02-23 18:55:01 -05:00
|
|
|
project_id: project,
|
2015-06-23 01:24:39 -04:00
|
|
|
id: merge_request.iid,
|
|
|
|
format: format
|
2015-06-17 16:12:28 -04:00
|
|
|
end
|
|
|
|
|
2017-06-13 18:12:31 -04:00
|
|
|
it 'renders the commits template to a string' do
|
|
|
|
go format: 'json'
|
2015-06-17 16:12:28 -04:00
|
|
|
|
2017-06-13 18:12:31 -04:00
|
|
|
expect(response).to render_template('projects/merge_requests/_commits')
|
|
|
|
expect(json_response).to have_key('html')
|
2015-04-13 01:27:45 -04:00
|
|
|
end
|
|
|
|
end
|
2016-07-27 07:42:18 -04:00
|
|
|
|
2016-11-10 19:04:28 -05:00
|
|
|
describe 'GET pipelines' do
|
2017-01-27 08:45:56 -05:00
|
|
|
before do
|
|
|
|
create(:ci_pipeline, project: merge_request.source_project,
|
|
|
|
ref: merge_request.source_branch,
|
|
|
|
sha: merge_request.diff_head_sha)
|
2016-07-29 09:51:11 -04:00
|
|
|
|
2017-06-13 18:12:31 -04:00
|
|
|
get :pipelines,
|
|
|
|
namespace_id: project.namespace.to_param,
|
|
|
|
project_id: project,
|
|
|
|
id: merge_request.iid,
|
|
|
|
format: :json
|
2016-07-29 09:51:11 -04:00
|
|
|
end
|
|
|
|
|
2017-06-13 18:12:31 -04:00
|
|
|
it 'responds with serialized pipelines' do
|
2017-07-14 11:52:54 -04:00
|
|
|
expect(json_response['pipelines']).not_to be_empty
|
|
|
|
expect(json_response['count']['all']).to eq 1
|
2016-07-27 07:42:18 -04:00
|
|
|
end
|
|
|
|
end
|
2016-07-27 12:54:04 -04:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
describe 'POST remove_wip' do
|
|
|
|
before do
|
2016-09-08 05:18:41 -04:00
|
|
|
merge_request.title = merge_request.wip_title
|
|
|
|
merge_request.save
|
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
xhr :post, :remove_wip,
|
|
|
|
namespace_id: merge_request.project.namespace.to_param,
|
|
|
|
project_id: merge_request.project,
|
|
|
|
id: merge_request.iid,
|
|
|
|
format: :json
|
|
|
|
end
|
2016-09-08 05:18:41 -04:00
|
|
|
|
2017-05-09 00:15:34 -04:00
|
|
|
it 'removes the wip status' do
|
2016-09-08 05:18:41 -04:00
|
|
|
expect(merge_request.reload.title).to eq(merge_request.wipless_title)
|
2016-10-05 08:57:57 -04:00
|
|
|
end
|
2017-05-09 00:15:34 -04:00
|
|
|
|
|
|
|
it 'renders MergeRequest as JSON' do
|
|
|
|
expect(json_response.keys).to include('id', 'iid', 'description')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe 'POST cancel_merge_when_pipeline_succeeds' do
|
|
|
|
subject do
|
|
|
|
xhr :post, :cancel_merge_when_pipeline_succeeds,
|
|
|
|
namespace_id: merge_request.project.namespace.to_param,
|
|
|
|
project_id: merge_request.project,
|
|
|
|
id: merge_request.iid,
|
|
|
|
format: :json
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
|
|
|
|
mwps_service = double
|
|
|
|
|
|
|
|
allow(MergeRequests::MergeWhenPipelineSucceedsService)
|
|
|
|
.to receive(:new)
|
|
|
|
.and_return(mwps_service)
|
|
|
|
|
|
|
|
expect(mwps_service).to receive(:cancel).with(merge_request)
|
|
|
|
|
|
|
|
subject
|
|
|
|
end
|
|
|
|
|
|
|
|
it { is_expected.to have_http_status(:success) }
|
|
|
|
|
|
|
|
it 'renders MergeRequest as JSON' do
|
|
|
|
subject
|
|
|
|
|
|
|
|
expect(json_response.keys).to include('id', 'iid', 'description')
|
|
|
|
end
|
2016-09-01 08:59:10 -04:00
|
|
|
end
|
|
|
|
|
2016-08-08 18:30:01 -04:00
|
|
|
describe 'POST assign_related_issues' do
|
|
|
|
let(:issue1) { create(:issue, project: project) }
|
|
|
|
let(:issue2) { create(:issue, project: project) }
|
|
|
|
|
|
|
|
def post_assign_issues
|
|
|
|
merge_request.update!(description: "Closes #{issue1.to_reference} and #{issue2.to_reference}",
|
|
|
|
author: user,
|
|
|
|
source_branch: 'feature',
|
|
|
|
target_branch: 'master')
|
|
|
|
|
|
|
|
post :assign_related_issues,
|
|
|
|
namespace_id: project.namespace.to_param,
|
2017-02-23 18:55:01 -05:00
|
|
|
project_id: project,
|
2016-08-08 18:30:01 -04:00
|
|
|
id: merge_request.iid
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'shows a flash message on success' do
|
|
|
|
post_assign_issues
|
|
|
|
|
|
|
|
expect(flash[:notice]).to eq '2 issues have been assigned to you'
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'correctly pluralizes flash message on success' do
|
2017-05-04 08:11:15 -04:00
|
|
|
issue2.assignees = [user]
|
2016-08-08 18:30:01 -04:00
|
|
|
|
|
|
|
post_assign_issues
|
|
|
|
|
|
|
|
expect(flash[:notice]).to eq '1 issue has been assigned to you'
|
|
|
|
end
|
2016-10-07 12:16:42 -04:00
|
|
|
|
|
|
|
it 'calls MergeRequests::AssignIssuesService' do
|
2017-06-21 09:48:12 -04:00
|
|
|
expect(MergeRequests::AssignIssuesService).to receive(:new)
|
|
|
|
.with(project, user, merge_request: merge_request)
|
|
|
|
.and_return(double(execute: { count: 1 }))
|
2016-10-07 12:16:42 -04:00
|
|
|
|
|
|
|
post_assign_issues
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'is skipped when not signed in' do
|
|
|
|
project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
|
|
|
|
sign_out(:user)
|
|
|
|
|
|
|
|
expect(MergeRequests::AssignIssuesService).not_to receive(:new)
|
|
|
|
|
|
|
|
post_assign_issues
|
|
|
|
end
|
2016-08-08 18:30:01 -04:00
|
|
|
end
|
2016-10-13 08:23:18 -04:00
|
|
|
|
|
|
|
describe 'GET ci_environments_status' do
|
2016-10-19 08:49:09 -04:00
|
|
|
context 'the environment is from a forked project' do
|
2017-09-29 04:04:50 -04:00
|
|
|
let!(:forked) { fork_project(project, user, repository: true) }
|
2016-10-13 08:23:18 -04:00
|
|
|
let!(:environment) { create(:environment, project: forked) }
|
|
|
|
let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') }
|
|
|
|
let(:admin) { create(:admin) }
|
|
|
|
|
|
|
|
let(:merge_request) do
|
|
|
|
create(:merge_request, source_project: forked, target_project: project)
|
|
|
|
end
|
|
|
|
|
|
|
|
before do
|
|
|
|
get :ci_environments_status,
|
|
|
|
namespace_id: merge_request.project.namespace.to_param,
|
2017-02-23 18:55:01 -05:00
|
|
|
project_id: merge_request.project,
|
2016-10-13 08:23:18 -04:00
|
|
|
id: merge_request.iid, format: 'json'
|
|
|
|
end
|
|
|
|
|
|
|
|
it 'links to the environment on that project' do
|
2017-07-20 05:34:09 -04:00
|
|
|
expect(json_response.first['url']).to match /#{forked.full_path}/
|
2016-10-13 08:23:18 -04:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2016-12-21 05:32:37 -05:00
|
|
|
|
2017-03-17 02:58:12 -04:00
|
|
|
describe 'GET pipeline_status.json' do
|
2017-03-21 09:21:13 -04:00
|
|
|
context 'when head_pipeline exists' do
|
|
|
|
let!(:pipeline) do
|
2017-03-06 07:01:18 -05:00
|
|
|
create(:ci_pipeline, project: merge_request.source_project,
|
2017-03-06 10:42:39 -05:00
|
|
|
ref: merge_request.source_branch,
|
2017-05-19 16:51:07 -04:00
|
|
|
sha: merge_request.diff_head_sha,
|
|
|
|
head_pipeline_of: merge_request)
|
2017-03-06 07:01:18 -05:00
|
|
|
end
|
|
|
|
|
2017-03-21 09:21:13 -04:00
|
|
|
let(:status) { pipeline.detailed_status(double('user')) }
|
|
|
|
|
2017-03-28 16:04:14 -04:00
|
|
|
before do
|
|
|
|
get_pipeline_status
|
|
|
|
end
|
2017-03-21 09:21:13 -04:00
|
|
|
|
|
|
|
it 'return a detailed head_pipeline status in json' do
|
2017-03-06 07:01:18 -05:00
|
|
|
expect(response).to have_http_status(:ok)
|
2017-03-11 09:30:25 -05:00
|
|
|
expect(json_response['text']).to eq status.text
|
|
|
|
expect(json_response['label']).to eq status.label
|
|
|
|
expect(json_response['icon']).to eq status.icon
|
2017-10-03 10:47:56 -04:00
|
|
|
expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico"
|
2017-03-06 07:01:18 -05:00
|
|
|
end
|
|
|
|
end
|
2017-03-21 09:21:13 -04:00
|
|
|
|
|
|
|
context 'when head_pipeline does not exist' do
|
2017-06-14 14:18:56 -04:00
|
|
|
before do
|
|
|
|
get_pipeline_status
|
|
|
|
end
|
2017-03-21 09:21:13 -04:00
|
|
|
|
|
|
|
it 'return empty' do
|
|
|
|
expect(response).to have_http_status(:ok)
|
|
|
|
expect(json_response).to be_empty
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_pipeline_status
|
|
|
|
get :pipeline_status, namespace_id: project.namespace,
|
|
|
|
project_id: project,
|
|
|
|
id: merge_request.iid,
|
|
|
|
format: :json
|
|
|
|
end
|
2017-03-06 07:01:18 -05:00
|
|
|
end
|
2012-11-24 18:04:13 -05:00
|
|
|
end
|