Commit c9871e84 authored by Francisco Javier López's avatar Francisco Javier López Committed by Douwe Maan

The API isn't using the appropriate services for managing forks

parent af09fb85
module Projects module Projects
class ForkService < BaseService class ForkService < BaseService
def execute def execute(fork_to_project = nil)
if fork_to_project
link_existing_project(fork_to_project)
else
fork_new_project
end
end
private
def link_existing_project(fork_to_project)
return if fork_to_project.forked?
link_fork_network(fork_to_project)
fork_to_project
end
def fork_new_project
new_params = { new_params = {
forked_from_project_id: @project.id, forked_from_project_id: @project.id,
visibility_level: allowed_visibility_level, visibility_level: allowed_visibility_level,
...@@ -21,15 +39,11 @@ module Projects ...@@ -21,15 +39,11 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level) new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
refresh_forks_count
link_fork_network(new_project) link_fork_network(new_project)
new_project new_project
end end
private
def fork_network def fork_network
if @project.fork_network if @project.fork_network
@project.fork_network @project.fork_network
...@@ -43,9 +57,17 @@ module Projects ...@@ -43,9 +57,17 @@ module Projects
end end
end end
def link_fork_network(new_project) def link_fork_network(fork_to_project)
fork_network.fork_network_members.create(project: new_project, fork_network.fork_network_members.create(project: fork_to_project,
forked_from_project: @project) forked_from_project: @project)
# TODO: remove this when ForkedProjectLink model is removed
unless fork_to_project.forked_project_link
fork_to_project.create_forked_project_link(forked_to_project: fork_to_project,
forked_from_project: @project)
end
refresh_forks_count
end end
def refresh_forks_count def refresh_forks_count
......
---
title: Using appropiate services in the API for managing forks
merge_request: 15709
author:
type: fixed
class RescheduleForkNetworkCreationCaller < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'PopulateForkNetworksRange'.freeze
BATCH_SIZE = 100
DELAY_INTERVAL = 15.seconds
disable_ddl_transaction!
class ForkedProjectLink < ActiveRecord::Base
include EachBatch
self.table_name = 'forked_project_links'
end
def up
say 'Populating the `fork_networks` based on existing `forked_project_links`'
queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
end
def down
# nothing
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171124150326) do ActiveRecord::Schema.define(version: 20171205190711) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -367,15 +367,16 @@ module API ...@@ -367,15 +367,16 @@ module API
post ":id/fork/:forked_from_id" do post ":id/fork/:forked_from_id" do
authenticated_as_admin! authenticated_as_admin!
forked_from_project = find_project!(params[:forked_from_id]) fork_from_project = find_project!(params[:forked_from_id])
not_found!("Source Project") unless forked_from_project
if user_project.forked_from_project.nil? not_found!("Source Project") unless fork_from_project
user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
::Projects::ForksCountService.new(forked_from_project).refresh_cache result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project)
if result
present user_project.reload, with: Entities::Project
else else
render_api_error!("Project already forked", 409) render_api_error!("Project already forked", 409) if user_project.forked?
end end
end end
...@@ -383,11 +384,11 @@ module API ...@@ -383,11 +384,11 @@ module API
delete ":id/fork" do delete ":id/fork" do
authorize! :remove_fork_project, user_project authorize! :remove_fork_project, user_project
if user_project.forked? result = destroy_conditionally!(user_project) do
destroy_conditionally!(user_project.forked_project_link) ::Projects::UnlinkForkService.new(user_project, current_user).execute
else
not_modified!
end end
result ? status(204) : not_modified!
end end
desc 'Share the project with a group' do desc 'Share the project with a group' do
......
...@@ -3,210 +3,253 @@ require 'spec_helper' ...@@ -3,210 +3,253 @@ require 'spec_helper'
describe Projects::ForkService do describe Projects::ForkService do
include ProjectForksHelper include ProjectForksHelper
let(:gitlab_shell) { Gitlab::Shell.new } let(:gitlab_shell) { Gitlab::Shell.new }
context 'when forking a new project' do
describe 'fork by user' do
before do
@from_user = create(:user)
@from_namespace = @from_user.namespace
avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
@from_project = create(:project,
:repository,
creator_id: @from_user.id,
namespace: @from_namespace,
star_count: 107,
avatar: avatar,
description: 'wow such project')
@to_user = create(:user)
@to_namespace = @to_user.namespace
@from_project.add_user(@to_user, :developer)
end
describe 'fork by user' do context 'fork project' do
before do context 'when forker is a guest' do
@from_user = create(:user) before do
@from_namespace = @from_user.namespace @guest = create(:user)
avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") @from_project.add_user(@guest, :guest)
@from_project = create(:project, end
:repository, subject { fork_project(@from_project, @guest) }
creator_id: @from_user.id,
namespace: @from_namespace,
star_count: 107,
avatar: avatar,
description: 'wow such project')
@to_user = create(:user)
@to_namespace = @to_user.namespace
@from_project.add_user(@to_user, :developer)
end
context 'fork project' do it { is_expected.not_to be_persisted }
context 'when forker is a guest' do it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
before do
@guest = create(:user)
@from_project.add_user(@guest, :guest)
end end
subject { fork_project(@from_project, @guest) }
it { is_expected.not_to be_persisted } describe "successfully creates project in the user namespace" do
it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) } let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) }
end
describe "successfully creates project in the user namespace" do it { expect(to_project).to be_persisted }
let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) } it { expect(to_project.errors).to be_empty }
it { expect(to_project.owner).to eq(@to_user) }
it { expect(to_project).to be_persisted } it { expect(to_project.namespace).to eq(@to_user.namespace) }
it { expect(to_project.errors).to be_empty } it { expect(to_project.star_count).to be_zero }
it { expect(to_project.owner).to eq(@to_user) } it { expect(to_project.description).to eq(@from_project.description) }
it { expect(to_project.namespace).to eq(@to_user.namespace) } it { expect(to_project.avatar.file).to be_exists }
it { expect(to_project.star_count).to be_zero }
it { expect(to_project.description).to eq(@from_project.description) }
it { expect(to_project.avatar.file).to be_exists }
# This test is here because we had a bug where the from-project lost its
# avatar after being forked.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
it "after forking the from-project still has its avatar" do
# If we do not fork the project first we cannot detect the bug.
expect(to_project).to be_persisted
expect(@from_project.avatar.file).to be_exists
end
it 'flushes the forks count cache of the source project' do # This test is here because we had a bug where the from-project lost its
expect(@from_project.forks_count).to be_zero # avatar after being forked.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
it "after forking the from-project still has its avatar" do
# If we do not fork the project first we cannot detect the bug.
expect(to_project).to be_persisted
fork_project(@from_project, @to_user) expect(@from_project.avatar.file).to be_exists
end
expect(@from_project.forks_count).to eq(1) it 'flushes the forks count cache of the source project' do
end expect(@from_project.forks_count).to be_zero
it 'creates a fork network with the new project and the root project set' do fork_project(@from_project, @to_user)
to_project
fork_network = @from_project.reload.fork_network
expect(fork_network).not_to be_nil expect(@from_project.forks_count).to eq(1)
expect(fork_network.root_project).to eq(@from_project) end
expect(fork_network.projects).to contain_exactly(@from_project, to_project)
end
end
context 'creating a fork of a fork' do it 'creates a fork network with the new project and the root project set' do
let(:from_forked_project) { fork_project(@from_project, @to_user) } to_project
let(:other_namespace) do fork_network = @from_project.reload.fork_network
group = create(:group)
group.add_owner(@to_user)
group
end
let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
it 'sets the root of the network to the root project' do expect(fork_network).not_to be_nil
expect(to_project.fork_network.root_project).to eq(@from_project) expect(fork_network.root_project).to eq(@from_project)
expect(fork_network.projects).to contain_exactly(@from_project, to_project)
end
end end
it 'sets the forked_from_project on the membership' do context 'creating a fork of a fork' do
expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project) let(:from_forked_project) { fork_project(@from_project, @to_user) }
let(:other_namespace) do
group = create(:group)
group.add_owner(@to_user)
group
end
let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
it 'sets the root of the network to the root project' do
expect(to_project.fork_network.root_project).to eq(@from_project)
end
it 'sets the forked_from_project on the membership' do
expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
end
end end
end end
end
context 'project already exists' do context 'project already exists' do
it "fails due to validation, not transaction failure" do it "fails due to validation, not transaction failure" do
@existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user, namespace: @to_namespace) @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace)
expect(@existing_project).to be_persisted expect(@existing_project).to be_persisted
expect(@to_project).not_to be_persisted expect(@to_project).not_to be_persisted
expect(@to_project.errors[:name]).to eq(['has already been taken']) expect(@to_project.errors[:name]).to eq(['has already been taken'])
expect(@to_project.errors[:path]).to eq(['has already been taken']) expect(@to_project.errors[:path]).to eq(['has already been taken'])
end
end end
end
context 'repository already exists' do context 'repository already exists' do
let(:repository_storage) { 'default' } let(:repository_storage) { 'default' }
let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
before do before do
gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}") gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
end end
after do after do
gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}") gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
end end
it 'does not allow creation' do it 'does not allow creation' do
to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace) to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
expect(to_project).not_to be_persisted expect(to_project).not_to be_persisted
expect(to_project.errors.messages).to have_key(:base) expect(to_project.errors.messages).to have_key(:base)
expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk') expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
end
end end
end
context 'GitLab CI is enabled' do context 'GitLab CI is enabled' do
it "forks and enables CI for fork" do it "forks and enables CI for fork" do
@from_project.enable_ci @from_project.enable_ci
@to_project = fork_project(@from_project, @to_user) @to_project = fork_project(@from_project, @to_user)
expect(@to_project.builds_enabled?).to be_truthy expect(@to_project.builds_enabled?).to be_truthy
end
end end
end
context "when project has restricted visibility level" do context "when project has restricted visibility level" do
context "and only one visibility level is restricted" do context "and only one visibility level is restricted" do
before do before do
@from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL) @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
end
it "creates fork with highest allowed level" do
forked_project = fork_project(@from_project, @to_user)
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
end
end end
it "creates fork with highest allowed level" do context "and all visibility levels are restricted" do
forked_project = fork_project(@from_project, @to_user) before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE])
end
it "creates fork with private visibility levels" do
forked_project = fork_project(@from_project, @to_user)
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
end end
end end
end
context "and all visibility levels are restricted" do describe 'fork to namespace' do
before do before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE]) @group_owner = create(:user)
@developer = create(:user)
@project = create(:project, :repository,
creator_id: @group_owner.id,
star_count: 777,
description: 'Wow, such a cool project!')
@group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
@project.add_user(@developer, :developer)
@project.add_user(@group_owner, :developer)
@opts = { namespace: @group }
end
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, @opts)
expect(to_project).to be_persisted
expect(to_project.errors).to be_empty
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
expect(to_project.description).to eq(@project.description)
expect(to_project.star_count).to be_zero
end end
end
it "creates fork with private visibility levels" do context 'fork project for group when user not owner' do
forked_project = fork_project(@from_project, @to_user) it 'group developer fails to fork project into the group' do
to_project = fork_project(@project, @developer, @opts)
expect(to_project.errors[:namespace]).to eq(['is not valid'])
end
end
expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) context 'project already exists in group' do
it 'fails due to validation, not transaction failure' do
existing_project = create(:project, :repository,
name: @project.name,
namespace: @group)
to_project = fork_project(@project, @group_owner, @opts)
expect(existing_project.persisted?).to be_truthy
expect(to_project.errors[:name]).to eq(['has already been taken'])
expect(to_project.errors[:path]).to eq(['has already been taken'])
end end
end end
end end
end end
describe 'fork to namespace' do context 'when linking fork to an existing project' do
before do let(:fork_from_project) { create(:project, :public) }
@group_owner = create(:user) let(:fork_to_project) { create(:project, :public) }
@developer = create(:user) let(:user) { create(:user) }
@project = create(:project, :repository,
creator_id: @group_owner.id, subject { described_class.new(fork_from_project, user) }
star_count: 777,
description: 'Wow, such a cool project!') def forked_from_project(project)
@group = create(:group) project.fork_network_member&.forked_from_project
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
@project.add_user(@developer, :developer)
@project.add_user(@group_owner, :developer)
@opts = { namespace: @group }
end end
context 'fork project for group' do context 'if project is already forked' do
it 'group owner successfully forks project into the group' do it 'does not create fork relation' do
to_project = fork_project(@project, @group_owner, @opts) allow(fork_to_project).to receive(:forked?).and_return(true)
expect(forked_from_project(fork_to_project)).to be_nil
expect(to_project).to be_persisted expect(subject.execute(fork_to_project)).to be_nil
expect(to_project.errors).to be_empty expect(forked_from_project(fork_to_project)).to be_nil
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
expect(to_project.description).to eq(@project.description)
expect(to_project.star_count).to be_zero
end end
end end
context 'fork project for group when user not owner' do context 'if project is not forked' do
it 'group developer fails to fork project into the group' do it 'creates fork relation' do
to_project = fork_project(@project, @developer, @opts) expect(fork_to_project.forked?).to be false
expect(to_project.errors[:namespace]).to eq(['is not valid']) expect(forked_from_project(fork_to_project)).to be_nil
subject.execute(fork_to_project)
expect(fork_to_project.forked?).to be true
expect(forked_from_project(fork_to_project)).to eq fork_from_project
expect(fork_to_project.forked_from_project).to eq fork_from_project
end end
end
context 'project already exists in group' do it 'flushes the forks count cache of the source project' do
it 'fails due to validation, not transaction failure' do expect(fork_from_project.forks_count).to be_zero
existing_project = create(:project, :repository,
name: @project.name, subject.execute(fork_to_project)
namespace: @group)
to_project = fork_project(@project, @group_owner, @opts) expect(fork_from_project.forks_count).to eq(1)
expect(existing_project.persisted?).to be_truthy
expect(to_project.errors[:name]).to eq(['has already been taken'])
expect(to_project.errors[:path]).to eq(['has already been taken'])
end end
end end
end end
......
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