Commit d17396b0 authored by Nick Thomas's avatar Nick Thomas

Allow comparisons of branches across projects

This is already permitted in the merge request view, for creating fork
merge requests, so it should be permitted in the repository view too.
parent e76567e8
...@@ -6,6 +6,7 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -6,6 +6,7 @@ class Projects::CompareController < Projects::ApplicationController
include DiffForPath include DiffForPath
include DiffHelper include DiffHelper
include RendersCommits include RendersCommits
include CompareHelper
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
...@@ -37,16 +38,18 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -37,16 +38,18 @@ class Projects::CompareController < Projects::ApplicationController
end end
def create def create
if params[:from].blank? || params[:to].blank? from_to_vars = {
from: params[:from].presence,
to: params[:to].presence,
from_project_id: params[:from_project_id].presence
}
if from_to_vars[:from].blank? || from_to_vars[:to].blank?
flash[:alert] = "You must select a Source and a Target revision" flash[:alert] = "You must select a Source and a Target revision"
from_to_vars = {
from: params[:from].presence, redirect_to project_compare_index_path(source_project, from_to_vars)
to: params[:to].presence
}
redirect_to project_compare_index_path(@project, from_to_vars)
else else
redirect_to project_compare_path(@project, redirect_to project_compare_path(source_project, from_to_vars)
params[:from], params[:to])
end end
end end
...@@ -73,13 +76,34 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -73,13 +76,34 @@ class Projects::CompareController < Projects::ApplicationController
return if valid.all? return if valid.all?
flash[:alert] = "Invalid branch name" flash[:alert] = "Invalid branch name"
redirect_to project_compare_index_path(@project) redirect_to project_compare_index_path(source_project)
end
# target == start_ref == from
def target_project
strong_memoize(:target_project) do
next source_project unless params.key?(:from_project_id)
next source_project unless Feature.enabled?(:compare_repo_dropdown, source_project, default_enabled: :yaml)
next source_project if params[:from_project_id].to_i == source_project.id
target_project = target_projects(source_project).find_by_id(params[:from_project_id])
# Just ignore the field if it points at a non-existent or hidden project
next source_project unless target_project && can?(current_user, :download_code, target_project)
target_project
end
end
# source == head_ref == to
def source_project
project
end end
def compare def compare
return @compare if defined?(@compare) return @compare if defined?(@compare)
@compare = CompareService.new(@project, head_ref).execute(@project, start_ref) @compare = CompareService.new(source_project, head_ref).execute(target_project, start_ref)
end end
def start_ref def start_ref
...@@ -102,9 +126,9 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -102,9 +126,9 @@ class Projects::CompareController < Projects::ApplicationController
def define_environment def define_environment
if compare if compare
environment_params = @repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit } environment_params = source_project.repository.branch_exists?(head_ref) ? { ref: head_ref } : { commit: compare.commit }
environment_params[:find_latest] = true environment_params[:find_latest] = true
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @environment = EnvironmentsFinder.new(source_project, current_user, environment_params).execute.last
end end
end end
...@@ -114,8 +138,8 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -114,8 +138,8 @@ class Projects::CompareController < Projects::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def merge_request def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened @merge_request ||= MergeRequestsFinder.new(current_user, project_id: target_project.id).execute.opened
.find_by(source_project: @project, source_branch: head_ref, target_branch: start_ref) .find_by(source_project: source_project, source_branch: head_ref, target_branch: start_ref)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
...@@ -5,29 +5,30 @@ class MergeRequestTargetProjectFinder ...@@ -5,29 +5,30 @@ class MergeRequestTargetProjectFinder
attr_reader :current_user, :source_project attr_reader :current_user, :source_project
def initialize(current_user: nil, source_project:) def initialize(current_user: nil, source_project:, project_feature: :merge_requests)
@current_user = current_user @current_user = current_user
@source_project = source_project @source_project = source_project
@project_feature = project_feature
end end
# rubocop: disable CodeReuse/ActiveRecord
def execute(include_routes: false) def execute(include_routes: false)
if source_project.fork_network if source_project.fork_network
include_routes ? projects.inc_routes : projects include_routes ? projects.inc_routes : projects
else else
Project.where(id: source_project) Project.id_in(source_project.id)
end end
end end
# rubocop: enable CodeReuse/ActiveRecord
private private
attr_reader :project_feature
def projects def projects
source_project source_project
.fork_network .fork_network
.projects .projects
.public_or_visible_to_user(current_user) .public_or_visible_to_user(current_user)
.non_archived .non_archived
.with_feature_available_for_user(:merge_requests, current_user) .with_feature_available_for_user(project_feature, current_user)
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module CompareHelper module CompareHelper
def create_mr_button?(from = params[:from], to = params[:to], project = @project) def create_mr_button?(from: params[:from], to: params[:to], source_project: @project, target_project: @target_project)
from.present? && from.present? &&
to.present? && to.present? &&
from != to && from != to &&
can?(current_user, :create_merge_request_from, project) && can?(current_user, :create_merge_request_from, source_project) &&
project.repository.branch_exists?(from) && can?(current_user, :create_merge_request_in, target_project) &&
project.repository.branch_exists?(to) target_project.repository.branch_exists?(from) &&
source_project.repository.branch_exists?(to)
end end
def create_mr_path(from = params[:from], to = params[:to], project = @project) def create_mr_path(from: params[:from], to: params[:to], source_project: @project, target_project: @target_project)
project_new_merge_request_path( project_new_merge_request_path(
project, target_project,
merge_request: { merge_request: {
source_project_id: source_project.id,
source_branch: to, source_branch: to,
target_project_id: target_project.id,
target_branch: from target_branch: from
} }
) )
end end
def target_projects(source_project)
MergeRequestTargetProjectFinder
.new(current_user: current_user, source_project: source_project, project_feature: :repository)
.execute(include_routes: true)
end
end end
...@@ -21,7 +21,8 @@ ...@@ -21,7 +21,8 @@
%ul.content-list.event_commits %ul.content-list.event_commits
= render "events/commit", project: project, event: event = render "events/commit", project: project, event: event
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user) - create_mr = event.new_ref? && create_mr_button?(from: project.default_branch, to: event.ref_name, source_project: project, target_project: project) && event.authored_by?(current_user)
- create_mr_path = create_mr_path(from: project.default_branch, to: event.ref_name, source_project: project, target_project: project) if create_mr
- if event.commits_count > 1 - if event.commits_count > 1
%li.commits-stat %li.commits-stat
%span ... and #{pluralize(event.commits_count - 1, 'more commit')}. %span ... and #{pluralize(event.commits_count - 1, 'more commit')}.
...@@ -40,9 +41,9 @@ ...@@ -40,9 +41,9 @@
- if create_mr - if create_mr
%span %span
or or
= link_to create_mr_path(project.default_branch, event.ref_name, project) do = link_to create_mr_path do
create a merge request create a merge request
- elsif create_mr - elsif create_mr
%li.commits-stat %li.commits-stat
= link_to create_mr_path(project.default_branch, event.ref_name, project) do = link_to create_mr_path do
Create Merge Request Create Merge Request
...@@ -35,8 +35,8 @@ ...@@ -35,8 +35,8 @@
.gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5 .gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5
%svg.s24 %svg.s24
- if merge_project && create_mr_button?(@repository.root_ref, branch.name) - if merge_project && create_mr_button?(from: @repository.root_ref, to: branch.name, source_project: @project, target_project: @project)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'gl-button btn btn-default' do = link_to create_mr_path(from: @repository.root_ref, to: branch.name, source_project: @project, target_project: @project), class: 'gl-button btn btn-default' do
= _('Merge request') = _('Merge request')
- if branch.name != @repository.root_ref - if branch.name != @repository.root_ref
......
...@@ -18,9 +18,9 @@ ...@@ -18,9 +18,9 @@
- if @merge_request.present? - if @merge_request.present?
.control.d-none.d-md-block .control.d-none.d-md-block
= link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'btn gl-button' = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'btn gl-button'
- elsif create_mr_button?(@repository.root_ref, @ref) - elsif create_mr_button?(from: @repository.root_ref, to: @ref, source_project: @project, target_project: @project)
.control.d-none.d-md-block .control.d-none.d-md-block
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn gl-button btn-success' = link_to _("Create merge request"), create_mr_path(from: @repository.root_ref, to: @ref, source_project: @project, target_project: @project), class: 'btn gl-button btn-success'
.control .control
= form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do
......
---
name: compare_repo_dropdown
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/issues/14615
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322141
milestone: '13.9'
type: development
group: group::source code
default_enabled: false
...@@ -3,11 +3,13 @@ ...@@ -3,11 +3,13 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Merge Request button' do RSpec.describe 'Merge Request button' do
shared_examples 'Merge request button only shown when allowed' do include ProjectForksHelper
let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) } let_it_be(:user) { create(:user) }
let(:forked_project) { create(:project, :public, :repository, forked_from_project: project) } let_it_be(:project) { create(:project, :public, :repository) }
let(:forked_project) { fork_project(project, user, repository: true) }
shared_examples 'Merge request button only shown when allowed' do
context 'not logged in' do context 'not logged in' do
it 'does not show Create merge request button' do it 'does not show Create merge request button' do
visit url visit url
...@@ -23,9 +25,15 @@ RSpec.describe 'Merge Request button' do ...@@ -23,9 +25,15 @@ RSpec.describe 'Merge Request button' do
end end
it 'shows Create merge request button' do it 'shows Create merge request button' do
href = project_new_merge_request_path(project, href = project_new_merge_request_path(
merge_request: { source_branch: 'feature', project,
target_branch: 'master' }) merge_request: {
source_project_id: project.id,
source_branch: 'feature',
target_project_id: project.id,
target_branch: 'master'
}
)
visit url visit url
...@@ -75,12 +83,16 @@ RSpec.describe 'Merge Request button' do ...@@ -75,12 +83,16 @@ RSpec.describe 'Merge Request button' do
end end
context 'on own fork of project' do context 'on own fork of project' do
let(:user) { forked_project.owner }
it 'shows Create merge request button' do it 'shows Create merge request button' do
href = project_new_merge_request_path(forked_project, href = project_new_merge_request_path(
merge_request: { source_branch: 'feature', forked_project,
target_branch: 'master' }) merge_request: {
source_project_id: forked_project.id,
source_branch: 'feature',
target_project_id: forked_project.id,
target_branch: 'master'
}
)
visit fork_url visit fork_url
...@@ -101,11 +113,33 @@ RSpec.describe 'Merge Request button' do ...@@ -101,11 +113,33 @@ RSpec.describe 'Merge Request button' do
end end
context 'on compare page' do context 'on compare page' do
let(:label) { 'Create merge request' }
it_behaves_like 'Merge request button only shown when allowed' do it_behaves_like 'Merge request button only shown when allowed' do
let(:label) { 'Create merge request' }
let(:url) { project_compare_path(project, from: 'master', to: 'feature') } let(:url) { project_compare_path(project, from: 'master', to: 'feature') }
let(:fork_url) { project_compare_path(forked_project, from: 'master', to: 'feature') } let(:fork_url) { project_compare_path(forked_project, from: 'master', to: 'feature') }
end end
it 'shows the correct merge request button when viewing across forks' do
sign_in(user)
project.add_developer(user)
href = project_new_merge_request_path(
project,
merge_request: {
source_project_id: forked_project.id,
source_branch: 'feature',
target_project_id: project.id,
target_branch: 'master'
}
)
visit project_compare_path(forked_project, from: 'master', to: 'feature', from_project_id: project.id)
within("#content-body") do
expect(page).to have_link(label, href: href)
end
end
end end
context 'on commits page' do context 'on commits page' do
......
...@@ -16,13 +16,22 @@ RSpec.describe MergeRequestTargetProjectFinder do ...@@ -16,13 +16,22 @@ RSpec.describe MergeRequestTargetProjectFinder do
expect(finder.execute).to contain_exactly(base_project, other_fork, forked_project) expect(finder.execute).to contain_exactly(base_project, other_fork, forked_project)
end end
it 'does not include projects that have merge requests turned off' do it 'does not include projects that have merge requests turned off by default' do
other_fork.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) other_fork.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
base_project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) base_project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
expect(finder.execute).to contain_exactly(forked_project) expect(finder.execute).to contain_exactly(forked_project)
end end
it 'includes projects that have merge requests turned off by default with a more-permissive project feature' do
finder = described_class.new(current_user: user, source_project: forked_project, project_feature: :repository)
other_fork.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
base_project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED)
expect(finder.execute).to contain_exactly(base_project, other_fork, forked_project)
end
it 'does not contain archived projects' do it 'does not contain archived projects' do
base_project.update!(archived: true) base_project.update!(archived: true)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment