Commit 0fff9db5 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'fj-41900-import-endpoint-with-overwrite-support' into 'master'

Extend API for importing a project export with overwrite support

Closes #41900

See merge request gitlab-org/gitlab-ce!17883
parents 44f4a674 f20912df
...@@ -286,6 +286,10 @@ class Group < Namespace ...@@ -286,6 +286,10 @@ class Group < Namespace
false false
end end
def refresh_project_authorizations
refresh_members_authorized_projects(blocking: false)
end
private private
def update_two_factor_requirement def update_two_factor_requirement
......
...@@ -252,6 +252,10 @@ class Namespace < ActiveRecord::Base ...@@ -252,6 +252,10 @@ class Namespace < ActiveRecord::Base
[] []
end end
def refresh_project_authorizations
owner.refresh_authorized_projects
end
private private
def path_or_parent_changed? def path_or_parent_changed?
......
...@@ -1472,7 +1472,9 @@ class Project < ActiveRecord::Base ...@@ -1472,7 +1472,9 @@ class Project < ActiveRecord::Base
end end
def rename_repo_notify! def rename_repo_notify!
send_move_instructions(full_path_was) # When we import a project overwriting the original project, there
# is a move operation. In that case we don't want to send the instructions.
send_move_instructions(full_path_was) unless started?
expires_full_path_cache expires_full_path_cache
self.old_path_with_namespace = full_path_was self.old_path_with_namespace = full_path_was
......
module Projects
class BaseMoveRelationsService < BaseService
attr_reader :source_project
def execute(source_project, remove_remaining_elements: true)
return if source_project.blank?
@source_project = source_project
true
end
private
def prepare_relation(relation, id_param = :id)
if Gitlab::Database.postgresql?
relation
else
relation.model.where("#{id_param}": relation.pluck(id_param))
end
end
end
end
...@@ -46,6 +46,20 @@ module Projects ...@@ -46,6 +46,20 @@ module Projects
raise raise
end end
def attempt_repositories_rollback
return unless @project
flush_caches(@project)
unless mv_repository(removal_path(repo_path), repo_path)
raise_error('Failed to restore project repository. Please contact the administrator.')
end
unless mv_repository(removal_path(wiki_path), wiki_path)
raise_error('Failed to restore wiki repository. Please contact the administrator.')
end
end
private private
def repo_path def repo_path
...@@ -70,12 +84,9 @@ module Projects ...@@ -70,12 +84,9 @@ module Projects
# Skip repository removal. We use this flag when remove user or group # Skip repository removal. We use this flag when remove user or group
return true if params[:skip_repo] == true return true if params[:skip_repo] == true
# There is a possibility project does not have repository or wiki
return true unless gitlab_shell.exists?(project.repository_storage_path, path + '.git')
new_path = removal_path(path) new_path = removal_path(path)
if gitlab_shell.mv_repository(project.repository_storage_path, path, new_path) if mv_repository(path, new_path)
log_info("Repository \"#{path}\" moved to \"#{new_path}\"") log_info("Repository \"#{path}\" moved to \"#{new_path}\"")
project.run_after_commit do project.run_after_commit do
...@@ -87,6 +98,13 @@ module Projects ...@@ -87,6 +98,13 @@ module Projects
end end
end end
def mv_repository(from_path, to_path)
# There is a possibility project does not have repository or wiki
return true unless gitlab_shell.exists?(project.repository_storage_path, from_path + '.git')
gitlab_shell.mv_repository(project.repository_storage_path, from_path, to_path)
end
def attempt_rollback(project, message) def attempt_rollback(project, message)
return unless project return unless project
......
...@@ -15,9 +15,18 @@ module Projects ...@@ -15,9 +15,18 @@ module Projects
file = params.delete(:file) file = params.delete(:file)
FileUtils.copy_entry(file.path, import_upload_path) FileUtils.copy_entry(file.path, import_upload_path)
@overwrite = params.delete(:overwrite)
data = {}
data[:override_params] = @override_params if @override_params
if overwrite_project?
data[:original_path] = params[:path]
params[:path] += "-#{tmp_filename}"
end
params[:import_type] = 'gitlab_project' params[:import_type] = 'gitlab_project'
params[:import_source] = import_upload_path params[:import_source] = import_upload_path
params[:import_data] = { data: { override_params: @override_params } } if @override_params params[:import_data] = { data: data } if data.present?
::Projects::CreateService.new(current_user, params).execute ::Projects::CreateService.new(current_user, params).execute
end end
...@@ -31,5 +40,17 @@ module Projects ...@@ -31,5 +40,17 @@ module Projects
def tmp_filename def tmp_filename
SecureRandom.hex SecureRandom.hex
end end
def overwrite_project?
@overwrite && project_with_same_full_path?
end
def project_with_same_full_path?
Project.find_by_full_path("#{current_namespace.full_path}/#{params[:path]}").present?
end
def current_namespace
@current_namespace ||= Namespace.find_by(id: params[:namespace_id])
end
end end
end end
module Projects
class MoveAccessService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
@project.with_transaction_returning_status do
if @project.namespace != source_project.namespace
@project.run_after_commit do
source_project.namespace.refresh_project_authorizations
self.namespace.refresh_project_authorizations
end
end
::Projects::MoveProjectMembersService.new(@project, @current_user)
.execute(source_project, remove_remaining_elements: remove_remaining_elements)
::Projects::MoveProjectGroupLinksService.new(@project, @current_user)
.execute(source_project, remove_remaining_elements: remove_remaining_elements)
::Projects::MoveProjectAuthorizationsService.new(@project, @current_user)
.execute(source_project, remove_remaining_elements: remove_remaining_elements)
success
end
end
end
end
module Projects
class MoveDeployKeysProjectsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_deploy_keys_projects
remove_remaining_deploy_keys_projects if remove_remaining_elements
success
end
end
private
def move_deploy_keys_projects
prepare_relation(non_existent_deploy_keys_projects)
.update_all(project_id: @project.id)
end
def non_existent_deploy_keys_projects
source_project.deploy_keys_projects
.joins(:deploy_key)
.where.not(keys: { fingerprint: @project.deploy_keys.select(:fingerprint) })
end
def remove_remaining_deploy_keys_projects
source_project.deploy_keys_projects.destroy_all
end
end
end
module Projects
class MoveForksService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super && source_project.fork_network
Project.transaction(requires_new: true) do
move_forked_project_links
move_fork_network_members
update_root_project
refresh_forks_count
success
end
end
private
def move_forked_project_links
# Update ancestor
ForkedProjectLink.where(forked_to_project: source_project)
.update_all(forked_to_project_id: @project.id)
# Update the descendants
ForkedProjectLink.where(forked_from_project: source_project)
.update_all(forked_from_project_id: @project.id)
end
def move_fork_network_members
ForkNetworkMember.where(project: source_project).update_all(project_id: @project.id)
ForkNetworkMember.where(forked_from_project: source_project).update_all(forked_from_project_id: @project.id)
end
def update_root_project
# Update root network project
ForkNetwork.where(root_project: source_project).update_all(root_project_id: @project.id)
end
def refresh_forks_count
Projects::ForksCountService.new(@project).refresh_cache
end
end
end
module Projects
class MoveLfsObjectsProjectsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_lfs_objects_projects
remove_remaining_lfs_objects_project if remove_remaining_elements
success
end
end
private
def move_lfs_objects_projects
prepare_relation(non_existent_lfs_objects_projects)
.update_all(project_id: @project.lfs_storage_project.id)
end
def remove_remaining_lfs_objects_project
source_project.lfs_objects_projects.destroy_all
end
def non_existent_lfs_objects_projects
source_project.lfs_objects_projects.where.not(lfs_object: @project.lfs_objects)
end
end
end
module Projects
class MoveNotificationSettingsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_notification_settings
remove_remaining_notification_settings if remove_remaining_elements
success
end
end
private
def move_notification_settings
prepare_relation(non_existent_notifications)
.update_all(source_id: @project.id)
end
# Remove remaining notification settings from source_project
def remove_remaining_notification_settings
source_project.notification_settings.destroy_all
end
# Get users of current notification_settings
def users_in_target_project
@project.notification_settings.select(:user_id)
end
# Look for notification_settings in source_project that are not in the target project
def non_existent_notifications
source_project.notification_settings
.select(:id)
.where.not(user_id: users_in_target_project)
end
end
end
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
# the authorizations if neccessary
module Projects
class MoveProjectAuthorizationsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_project_authorizations
remove_remaining_authorizations if remove_remaining_elements
success
end
end
private
def move_project_authorizations
prepare_relation(non_existent_authorization, :user_id)
.update_all(project_id: @project.id)
end
def remove_remaining_authorizations
# I think because the Project Authorization table does not have a primary key
# it brings a lot a problems/bugs. First, Rails raises PG::SyntaxException if we use
# destroy_all instead of delete_all.
source_project.project_authorizations.delete_all(:delete_all)
end
# Look for authorizations in source_project that are not in the target project
def non_existent_authorization
source_project.project_authorizations
.select(:user_id)
.where.not(user: @project.authorized_users)
end
end
end
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
# the authorizations if neccessary
module Projects
class MoveProjectGroupLinksService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_group_links
remove_remaining_project_group_links if remove_remaining_elements
success
end
end
private
def move_group_links
prepare_relation(non_existent_group_links)
.update_all(project_id: @project.id)
end
# Remove remaining project group links from source_project
def remove_remaining_project_group_links
source_project.reload.project_group_links.destroy_all
end
def group_links_in_target_project
@project.project_group_links.select(:group_id)
end
# Look for groups in source_project that are not in the target project
def non_existent_group_links
source_project.project_group_links
.where.not(group_id: group_links_in_target_project)
end
end
end
# NOTE: This service cannot be used directly because it is part of a
# a bigger process. Instead, use the service MoveAccessService which moves
# project memberships, project group links, authorizations and refreshes
# the authorizations if neccessary
module Projects
class MoveProjectMembersService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
Project.transaction(requires_new: true) do
move_project_members
remove_remaining_members if remove_remaining_elements
success
end
end
private
def move_project_members
prepare_relation(non_existent_members).update_all(source_id: @project.id)
end
def remove_remaining_members
# Remove remaining members and authorizations from source_project
source_project.project_members.destroy_all
end
def project_members_in_target_project
@project.project_members.select(:user_id)
end
# Look for members in source_project that are not in the target project
def non_existent_members
source_project.members
.select(:id)
.where.not(user_id: @project.project_members.select(:user_id))
end
end
end
module Projects
class MoveUsersStarProjectsService < BaseMoveRelationsService
def execute(source_project, remove_remaining_elements: true)
return unless super
user_stars = source_project.users_star_projects
return unless user_stars.any?
Project.transaction(requires_new: true) do
user_stars.update_all(project_id: @project.id)
Project.reset_counters @project.id, :users_star_projects
Project.reset_counters source_project.id, :users_star_projects
success
end
end
end
end
module Projects
class OverwriteProjectService < BaseService
def execute(source_project)
return unless source_project && source_project.namespace == @project.namespace
Project.transaction do
move_before_destroy_relationships(source_project)
destroy_old_project(source_project)
rename_project(source_project.name, source_project.path)
@project
end
# Projects::DestroyService can raise Exceptions, but we don't want
# to pass that kind of exception to the caller. Instead, we change it
# for a StandardError exception
rescue Exception => e # rubocop:disable Lint/RescueException
attempt_restore_repositories(source_project)
if e.class == Exception
raise StandardError, e.message
else
raise
end
end
private
def move_before_destroy_relationships(source_project)
options = { remove_remaining_elements: false }
::Projects::MoveUsersStarProjectsService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveAccessService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveDeployKeysProjectsService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveNotificationSettingsService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveForksService.new(@project, @current_user).execute(source_project, options)
::Projects::MoveLfsObjectsProjectsService.new(@project, @current_user).execute(source_project, options)
add_source_project_to_fork_network(source_project)
end
def destroy_old_project(source_project)
# Delete previous project (synchronously) and unlink relations
::Projects::DestroyService.new(source_project, @current_user).execute
end
def rename_project(name, path)
# Update de project's name and path to the original name/path
::Projects::UpdateService.new(@project,
@current_user,
{ name: name, path: path })
.execute
end
def attempt_restore_repositories(project)
::Projects::DestroyService.new(project, @current_user).attempt_repositories_rollback
end
def add_source_project_to_fork_network(source_project)
return unless @project.fork_network
# Because he have moved all references in the fork network from the source_project
# we won't be able to query the database (only through its cached data),
# for its former relationships. That's why we're adding it to the network
# as a fork of the target project
ForkNetworkMember.create!(fork_network: @project.fork_network,
project: source_project,
forked_from_project: @project)
end
end
end
---
title: Extend API for importing a project export with overwrite support
merge_request: 17883
author:
type: added
...@@ -111,6 +111,7 @@ POST /projects/import ...@@ -111,6 +111,7 @@ POST /projects/import
| `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the current user's namespace | | `namespace` | integer/string | no | The ID or path of the namespace that the project will be imported to. Defaults to the current user's namespace |
| `file` | string | yes | The file to be uploaded | | `file` | string | yes | The file to be uploaded |
| `path` | string | yes | Name and path for new project | | `path` | string | yes | Name and path for new project |
| `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false |
| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] | | `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] |
The override params passed will take precendence over all values defined inside the export file. The override params passed will take precendence over all values defined inside the export file.
......
...@@ -26,6 +26,7 @@ module API ...@@ -26,6 +26,7 @@ module API
requires :path, type: String, desc: 'The new project path and name' requires :path, type: String, desc: 'The new project path and name'
requires :file, type: File, desc: 'The project export file to be imported' requires :file, type: File, desc: 'The project export file to be imported'
optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace." optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace."
optional :overwrite, type: Boolean, default: false, desc: 'If there is a project in the same namespace and with the same name overwrite it'
optional :override_params, optional :override_params,
type: Hash, type: Hash,
desc: 'New project params to override values in the export' do desc: 'New project params to override values in the export' do
...@@ -50,7 +51,8 @@ module API ...@@ -50,7 +51,8 @@ module API
project_params = { project_params = {
path: import_params[:path], path: import_params[:path],
namespace_id: namespace.id, namespace_id: namespace.id,
file: import_params[:file]['tempfile'] file: import_params[:file]['tempfile'],
overwrite: import_params[:overwrite]
} }
override_params = import_params.delete(:override_params) override_params = import_params.delete(:override_params)
......
module Gitlab module Gitlab
module ImportExport module ImportExport
class Importer class Importer
include Gitlab::Allowable
include Gitlab::Utils::StrongMemoize
def self.imports_repository? def self.imports_repository?
true true
end end
...@@ -13,12 +16,14 @@ module Gitlab ...@@ -13,12 +16,14 @@ module Gitlab
end end
def execute def execute
if import_file && check_version! && restorers.all?(&:restore) if import_file && check_version! && restorers.all?(&:restore) && overwrite_project
project_tree.restored_project project_tree.restored_project
else else
raise Projects::ImportService::Error.new(@shared.errors.join(', ')) raise Projects::ImportService::Error.new(@shared.errors.join(', '))
end end
rescue => e
raise Projects::ImportService::Error.new(e.message)
ensure
remove_import_file remove_import_file
end end
...@@ -26,7 +31,7 @@ module Gitlab ...@@ -26,7 +31,7 @@ module Gitlab
def restorers def restorers
[repo_restorer, wiki_restorer, project_tree, avatar_restorer, [repo_restorer, wiki_restorer, project_tree, avatar_restorer,
uploads_restorer, lfs_restorer] uploads_restorer, lfs_restorer, statistics_restorer]
end end
def import_file def import_file
...@@ -69,6 +74,10 @@ module Gitlab ...@@ -69,6 +74,10 @@ module Gitlab
Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: @shared) Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: @shared)
end end
def statistics_restorer
Gitlab::ImportExport::StatisticsRestorer.new(project: project_tree.restored_project, shared: @shared)
end
def path_with_namespace def path_with_namespace
File.join(@project.namespace.full_path, @project.path) File.join(@project.namespace.full_path, @project.path)
end end
...@@ -84,6 +93,33 @@ module Gitlab ...@@ -84,6 +93,33 @@ module Gitlab
def remove_import_file def remove_import_file
FileUtils.rm_rf(@archive_file) FileUtils.rm_rf(@archive_file)
end end
def overwrite_project
project = project_tree.restored_project
return unless can?(@current_user, :admin_namespace, project.namespace)
if overwrite_project?
::Projects::OverwriteProjectService.new(project, @current_user)
.execute(project_to_overwrite)
end
true
end
def original_path
@project.import_data&.data&.fetch('original_path', nil)
end
def overwrite_project?
original_path.present? && project_to_overwrite.present?
end
def project_to_overwrite
strong_memoize(:project_to_overwrite) do
Project.find_by_full_path("#{@project.namespace.full_path}/#{original_path}")
end
end
end end
end end
end end
...@@ -92,7 +92,7 @@ module Gitlab ...@@ -92,7 +92,7 @@ module Gitlab
end end
def override_params def override_params
return {} unless params = @project.import_data&.data&.fetch('override_params') return {} unless params = @project.import_data&.data&.fetch('override_params', nil)
@override_params ||= params.select do |key, _value| @override_params ||= params.select do |key, _value|
Project.column_names.include?(key.to_s) && Project.column_names.include?(key.to_s) &&
......
module Gitlab
module ImportExport
class StatisticsRestorer
def initialize(project:, shared:)
@project = project
@shared = shared
end
def restore
@project.statistics.refresh!
rescue => e
@shared.error(e)
false
end
end
end
end
FactoryBot.define do
factory :users_star_project do
project
user
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::Importer do describe Gitlab::ImportExport::Importer do
let(:user) { create(:user) }
let(:test_path) { "#{Dir.tmpdir}/importer_spec" } let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
let(:shared) { project.import_export_shared } let(:shared) { project.import_export_shared }
let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) } let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) }
...@@ -11,6 +12,7 @@ describe Gitlab::ImportExport::Importer do ...@@ -11,6 +12,7 @@ describe Gitlab::ImportExport::Importer do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
FileUtils.mkdir_p(shared.export_path) FileUtils.mkdir_p(shared.export_path)
FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path) FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path)
allow(subject).to receive(:remove_import_file)
end end
after do after do
...@@ -42,7 +44,8 @@ describe Gitlab::ImportExport::Importer do ...@@ -42,7 +44,8 @@ describe Gitlab::ImportExport::Importer do
Gitlab::ImportExport::RepoRestorer, Gitlab::ImportExport::RepoRestorer,
Gitlab::ImportExport::WikiRestorer, Gitlab::ImportExport::WikiRestorer,
Gitlab::ImportExport::UploadsRestorer, Gitlab::ImportExport::UploadsRestorer,
Gitlab::ImportExport::LfsRestorer Gitlab::ImportExport::LfsRestorer,
Gitlab::ImportExport::StatisticsRestorer
].each do |restorer| ].each do |restorer|
it "calls the #{restorer}" do it "calls the #{restorer}" do
fake_restorer = double(restorer.to_s) fake_restorer = double(restorer.to_s)
...@@ -60,5 +63,42 @@ describe Gitlab::ImportExport::Importer do ...@@ -60,5 +63,42 @@ describe Gitlab::ImportExport::Importer do
importer.execute importer.execute
end end
end end
context 'when project successfully restored' do
let!(:existing_project) { create(:project, namespace: user.namespace) }
let(:project) { create(:project, namespace: user.namespace, name: 'whatever', path: 'whatever') }
before do
restorers = double
allow(subject).to receive(:import_file).and_return(true)
allow(subject).to receive(:check_version!).and_return(true)
allow(subject).to receive(:restorers).and_return(restorers)
allow(restorers).to receive(:all?).and_return(true)
allow(project).to receive(:import_data).and_return(double(data: { 'original_path' => existing_project.path }))
end
context 'when import_data' do
context 'has original_path' do
it 'overwrites existing project' do
expect_any_instance_of(::Projects::OverwriteProjectService).to receive(:execute).with(existing_project)
subject.execute
end
end
context 'has not original_path' do
before do
allow(project).to receive(:import_data).and_return(double(data: {}))
end
it 'does not call the overwrite service' do
expect_any_instance_of(::Projects::OverwriteProjectService).not_to receive(:execute).with(existing_project)
subject.execute
end
end
end
end
end end
end end
...@@ -114,6 +114,29 @@ describe API::ProjectImport do ...@@ -114,6 +114,29 @@ describe API::ProjectImport do
expect(import_project.description).to eq('Hello world') expect(import_project.description).to eq('Hello world')
end end
context 'when target path already exists in namespace' do
let(:existing_project) { create(:project, namespace: user.namespace) }
it 'does not schedule an import' do
expect_any_instance_of(Project).not_to receive(:import_schedule)
post api('/projects/import', user), path: existing_project.path, file: fixture_file_upload(file)
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']).to eq('Name has already been taken')
end
context 'when param overwrite is true' do
it 'schedules an import' do
stub_import(user.namespace)
post api('/projects/import', user), path: existing_project.path, file: fixture_file_upload(file), overwrite: true
expect(response).to have_gitlab_http_status(201)
end
end
end
def stub_import(namespace) def stub_import(namespace)
expect_any_instance_of(Project).to receive(:import_schedule) expect_any_instance_of(Project).to receive(:import_schedule)
expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original
......
...@@ -248,6 +248,28 @@ describe Projects::DestroyService do ...@@ -248,6 +248,28 @@ describe Projects::DestroyService do
end end
end end
context '#attempt_restore_repositories' do
let(:path) { project.disk_path + '.git' }
before do
expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy
expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
# Dont run sidekiq to check if renamed repository exists
Sidekiq::Testing.fake! { destroy_project(project, user, {}) }
expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_falsey
expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy
end
it 'restores the repositories' do
Sidekiq::Testing.fake! { described_class.new(project, user).attempt_repositories_rollback }
expect(project.gitlab_shell.exists?(project.repository_storage_path, path)).to be_truthy
expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey
end
end
def destroy_project(project, user, params = {}) def destroy_project(project, user, params = {})
if async if async
Projects::DestroyService.new(project, user, params).async_execute Projects::DestroyService.new(project, user, params).async_execute
......
...@@ -4,7 +4,8 @@ describe Projects::GitlabProjectsImportService do ...@@ -4,7 +4,8 @@ describe Projects::GitlabProjectsImportService do
set(:namespace) { create(:namespace) } set(:namespace) { create(:namespace) }
let(:path) { 'test-path' } let(:path) { 'test-path' }
let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
let(:import_params) { { namespace_id: namespace.id, path: path, file: file } } let(:overwrite) { false }
let(:import_params) { { namespace_id: namespace.id, path: path, file: file, overwrite: overwrite } }
subject { described_class.new(namespace.owner, import_params) } subject { described_class.new(namespace.owner, import_params) }
describe '#execute' do describe '#execute' do
...@@ -37,5 +38,28 @@ describe Projects::GitlabProjectsImportService do ...@@ -37,5 +38,28 @@ describe Projects::GitlabProjectsImportService do
expect(project.import_data.data['override_params']['description']).to eq('Hello') expect(project.import_data.data['override_params']['description']).to eq('Hello')
end end
end end
context 'when there is a project with the same path' do
let(:existing_project) { create(:project, namespace: namespace) }
let(:path) { existing_project.path}
it 'does not create the project' do
project = subject.execute
expect(project).to be_invalid
expect(project).not_to be_persisted
end
context 'when overwrite param is set' do
let(:overwrite) { true }
it 'creates a project in a temporary full_path' do
project = subject.execute
expect(project).to be_valid
expect(project).to be_persisted
end
end
end
end end
end end
require 'spec_helper'
describe Projects::MoveAccessService do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project_with_access) { create(:project, namespace: user.namespace) }
let(:master_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
let(:master_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
before do
project_with_access.add_master(master_user)
project_with_access.add_developer(developer_user)
project_with_access.add_reporter(reporter_user)
project_with_access.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
project_with_access.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
project_with_access.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER)
end
subject { described_class.new(target_project, user) }
describe '#execute' do
shared_examples 'move the accesses' do
it do
expect(project_with_access.project_members.count).to eq 4
expect(project_with_access.project_group_links.count).to eq 3
expect(project_with_access.authorized_users.count).to eq 4
subject.execute(project_with_access)
expect(project_with_access.project_members.count).to eq 0
expect(project_with_access.project_group_links.count).to eq 0
expect(project_with_access.authorized_users.count).to eq 1
expect(target_project.project_members.count).to eq 4
expect(target_project.project_group_links.count).to eq 3
expect(target_project.authorized_users.count).to eq 4
end
it 'rollbacks if an exception is raised' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_groups) }.to raise_error(StandardError)
expect(project_with_access.project_members.count).to eq 4
expect(project_with_access.project_group_links.count).to eq 3
expect(project_with_access.authorized_users.count).to eq 4
end
end
context 'when both projects are in the same namespace' do
let(:target_project) { create(:project, namespace: user.namespace) }
it 'does not refresh project owner authorized projects' do
allow(project_with_access).to receive(:namespace).and_return(user.namespace)
expect(project_with_access.namespace).not_to receive(:refresh_project_authorizations)
expect(target_project.namespace).not_to receive(:refresh_project_authorizations)
subject.execute(project_with_access)
end
it_behaves_like 'move the accesses'
end
context 'when projects are in different namespaces' do
let(:target_project) { create(:project, namespace: group) }
before do
group.add_owner(user)
end
it 'refreshes both project owner authorized projects' do
allow(project_with_access).to receive(:namespace).and_return(user.namespace)
expect(user.namespace).to receive(:refresh_project_authorizations).once
expect(group).to receive(:refresh_project_authorizations).once
subject.execute(project_with_access)
end
it_behaves_like 'move the accesses'
end
context 'when remove_remaining_elements is false' do
let(:target_project) { create(:project, namespace: user.namespace) }
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining memberships' do
target_project.add_master(master_user)
subject.execute(project_with_access, options)
expect(project_with_access.project_members.count).not_to eq 0
end
it 'does not remove remaining group links' do
target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
subject.execute(project_with_access, options)
expect(project_with_access.project_group_links.count).not_to eq 0
end
it 'does not remove remaining authorizations' do
target_project.add_developer(developer_user)
subject.execute(project_with_access, options)
expect(project_with_access.project_authorizations.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveDeployKeysProjectsService do
let!(:user) { create(:user) }
let!(:project_with_deploy_keys) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
create_list(:deploy_keys_project, 2, project: project_with_deploy_keys)
end
it 'moves the user\'s deploy keys from one project to another' do
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 2
expect(target_project.deploy_keys_projects.count).to eq 0
subject.execute(project_with_deploy_keys)
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 0
expect(target_project.deploy_keys_projects.count).to eq 2
end
it 'does not link existent deploy_keys in the current project' do
target_project.deploy_keys << project_with_deploy_keys.deploy_keys.first
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 2
expect(target_project.deploy_keys_projects.count).to eq 1
subject.execute(project_with_deploy_keys)
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 0
expect(target_project.deploy_keys_projects.count).to eq 2
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_deploy_keys) }.to raise_error(StandardError)
expect(project_with_deploy_keys.deploy_keys_projects.count).to eq 2
expect(target_project.deploy_keys_projects.count).to eq 0
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining deploy keys projects' do
target_project.deploy_keys << project_with_deploy_keys.deploy_keys.first
subject.execute(project_with_deploy_keys, options)
expect(project_with_deploy_keys.deploy_keys_projects.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveForksService do
include ProjectForksHelper
let!(:user) { create(:user) }
let!(:project_with_forks) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
let!(:lvl1_forked_project_1) { fork_project(project_with_forks, user) }
let!(:lvl1_forked_project_2) { fork_project(project_with_forks, user) }
let!(:lvl2_forked_project_1_1) { fork_project(lvl1_forked_project_1, user) }
let!(:lvl2_forked_project_1_2) { fork_project(lvl1_forked_project_1, user) }
subject { described_class.new(target_project, user) }
describe '#execute' do
context 'when moving a root forked project' do
it 'moves the descendant forks' do
expect(project_with_forks.forks.count).to eq 2
expect(target_project.forks.count).to eq 0
subject.execute(project_with_forks)
expect(project_with_forks.forks.count).to eq 0
expect(target_project.forks.count).to eq 2
expect(lvl1_forked_project_1.forked_from_project).to eq target_project
expect(lvl1_forked_project_1.fork_network_member.forked_from_project).to eq target_project
expect(lvl1_forked_project_2.forked_from_project).to eq target_project
expect(lvl1_forked_project_2.fork_network_member.forked_from_project).to eq target_project
end
it 'updates the fork network' do
expect(project_with_forks.fork_network.root_project).to eq project_with_forks
expect(project_with_forks.fork_network.fork_network_members.map(&:project)).to include project_with_forks
subject.execute(project_with_forks)
expect(target_project.reload.fork_network.root_project).to eq target_project
expect(target_project.fork_network.fork_network_members.map(&:project)).not_to include project_with_forks
end
end
context 'when moving a intermediate forked project' do
it 'moves the descendant forks' do
expect(lvl1_forked_project_1.forks.count).to eq 2
expect(target_project.forks.count).to eq 0
subject.execute(lvl1_forked_project_1)
expect(lvl1_forked_project_1.forks.count).to eq 0
expect(target_project.forks.count).to eq 2
expect(lvl2_forked_project_1_1.forked_from_project).to eq target_project
expect(lvl2_forked_project_1_1.fork_network_member.forked_from_project).to eq target_project
expect(lvl2_forked_project_1_2.forked_from_project).to eq target_project
expect(lvl2_forked_project_1_2.fork_network_member.forked_from_project).to eq target_project
end
it 'moves the ascendant fork' do
subject.execute(lvl1_forked_project_1)
expect(target_project.forked_from_project).to eq project_with_forks
expect(target_project.fork_network_member.forked_from_project).to eq project_with_forks
end
it 'does not update fork network' do
subject.execute(lvl1_forked_project_1)
expect(target_project.reload.fork_network.root_project).to eq project_with_forks
end
end
context 'when moving a leaf forked project' do
it 'moves the ascendant fork' do
subject.execute(lvl2_forked_project_1_1)
expect(target_project.forked_from_project).to eq lvl1_forked_project_1
expect(target_project.fork_network_member.forked_from_project).to eq lvl1_forked_project_1
end
it 'does not update fork network' do
subject.execute(lvl2_forked_project_1_1)
expect(target_project.reload.fork_network.root_project).to eq project_with_forks
end
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_forks) }.to raise_error(StandardError)
expect(project_with_forks.forks.count).to eq 2
expect(target_project.forks.count).to eq 0
end
end
end
require 'spec_helper'
describe Projects::MoveLfsObjectsProjectsService do
let!(:user) { create(:user) }
let!(:project_with_lfs_objects) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
subject { described_class.new(target_project, user) }
before do
create_list(:lfs_objects_project, 3, project: project_with_lfs_objects)
end
describe '#execute' do
it 'links the lfs objects from existent in source project' do
expect(target_project.lfs_objects.count).to eq 0
subject.execute(project_with_lfs_objects)
expect(project_with_lfs_objects.reload.lfs_objects.count).to eq 0
expect(target_project.reload.lfs_objects.count).to eq 3
end
it 'does not link existent lfs_object in the current project' do
target_project.lfs_objects << project_with_lfs_objects.lfs_objects.first(2)
expect(target_project.lfs_objects.count).to eq 2
subject.execute(project_with_lfs_objects)
expect(target_project.lfs_objects.count).to eq 3
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_lfs_objects) }.to raise_error(StandardError)
expect(project_with_lfs_objects.lfs_objects.count).to eq 3
expect(target_project.lfs_objects.count).to eq 0
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining lfs objects' do
target_project.lfs_objects << project_with_lfs_objects.lfs_objects.first(2)
subject.execute(project_with_lfs_objects, options)
expect(project_with_lfs_objects.lfs_objects.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveNotificationSettingsService do
let(:user) { create(:user) }
let(:project_with_notifications) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
subject { described_class.new(target_project, user) }
describe '#execute' do
context 'with notification settings' do
before do
create_list(:notification_setting, 2, source: project_with_notifications)
end
it 'moves the user\'s notification settings from one project to another' do
expect(project_with_notifications.notification_settings.count).to eq 3
expect(target_project.notification_settings.count).to eq 1
subject.execute(project_with_notifications)
expect(project_with_notifications.notification_settings.count).to eq 0
expect(target_project.notification_settings.count).to eq 3
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_notifications) }.to raise_error(StandardError)
expect(project_with_notifications.notification_settings.count).to eq 3
expect(target_project.notification_settings.count).to eq 1
end
end
it 'does not move existent notification settings in the current project' do
expect(project_with_notifications.notification_settings.count).to eq 1
expect(target_project.notification_settings.count).to eq 1
expect(user.notification_settings.count).to eq 2
subject.execute(project_with_notifications)
expect(user.notification_settings.count).to eq 1
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining notification settings' do
subject.execute(project_with_notifications, options)
expect(project_with_notifications.notification_settings.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveProjectAuthorizationsService do
let!(:user) { create(:user) }
let(:project_with_users) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
let(:master_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
project_with_users.add_master(master_user)
project_with_users.add_developer(developer_user)
project_with_users.add_reporter(reporter_user)
end
it 'moves the authorizations from one project to another' do
expect(project_with_users.authorized_users.count).to eq 4
expect(target_project.authorized_users.count).to eq 1
subject.execute(project_with_users)
expect(project_with_users.authorized_users.count).to eq 0
expect(target_project.authorized_users.count).to eq 4
end
it 'does not move existent authorizations to the current project' do
target_project.add_master(developer_user)
target_project.add_developer(reporter_user)
expect(project_with_users.authorized_users.count).to eq 4
expect(target_project.authorized_users.count).to eq 3
subject.execute(project_with_users)
expect(project_with_users.authorized_users.count).to eq 0
expect(target_project.authorized_users.count).to eq 4
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining project authorizations' do
target_project.add_master(developer_user)
target_project.add_developer(reporter_user)
subject.execute(project_with_users, options)
expect(project_with_users.project_authorizations.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveProjectGroupLinksService do
let!(:user) { create(:user) }
let(:project_with_groups) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
let(:master_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
project_with_groups.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
project_with_groups.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
project_with_groups.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER)
end
it 'moves the group links from one project to another' do
expect(project_with_groups.project_group_links.count).to eq 3
expect(target_project.project_group_links.count).to eq 0
subject.execute(project_with_groups)
expect(project_with_groups.project_group_links.count).to eq 0
expect(target_project.project_group_links.count).to eq 3
end
it 'does not move existent group links in the current project' do
target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
expect(project_with_groups.project_group_links.count).to eq 3
expect(target_project.project_group_links.count).to eq 2
subject.execute(project_with_groups)
expect(project_with_groups.project_group_links.count).to eq 0
expect(target_project.project_group_links.count).to eq 3
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_groups) }.to raise_error(StandardError)
expect(project_with_groups.project_group_links.count).to eq 3
expect(target_project.project_group_links.count).to eq 0
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining project group links' do
target_project.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
target_project.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
subject.execute(project_with_groups, options)
expect(project_with_groups.project_group_links.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveProjectMembersService do
let!(:user) { create(:user) }
let(:project_with_users) { create(:project, namespace: user.namespace) }
let(:target_project) { create(:project, namespace: user.namespace) }
let(:master_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
project_with_users.add_master(master_user)
project_with_users.add_developer(developer_user)
project_with_users.add_reporter(reporter_user)
end
it 'moves the members from one project to another' do
expect(project_with_users.project_members.count).to eq 4
expect(target_project.project_members.count).to eq 1
subject.execute(project_with_users)
expect(project_with_users.project_members.count).to eq 0
expect(target_project.project_members.count).to eq 4
end
it 'does not move existent members to the current project' do
target_project.add_master(developer_user)
target_project.add_developer(reporter_user)
expect(project_with_users.project_members.count).to eq 4
expect(target_project.project_members.count).to eq 3
subject.execute(project_with_users)
expect(project_with_users.project_members.count).to eq 0
expect(target_project.project_members.count).to eq 4
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_users) }.to raise_error(StandardError)
expect(project_with_users.project_members.count).to eq 4
expect(target_project.project_members.count).to eq 1
end
context 'when remove_remaining_elements is false' do
let(:options) { { remove_remaining_elements: false } }
it 'does not remove remaining project members' do
target_project.add_master(developer_user)
target_project.add_developer(reporter_user)
subject.execute(project_with_users, options)
expect(project_with_users.project_members.count).not_to eq 0
end
end
end
end
require 'spec_helper'
describe Projects::MoveUsersStarProjectsService do
let!(:user) { create(:user) }
let!(:project_with_stars) { create(:project, namespace: user.namespace) }
let!(:target_project) { create(:project, namespace: user.namespace) }
subject { described_class.new(target_project, user) }
describe '#execute' do
before do
create_list(:users_star_project, 2, project: project_with_stars)
end
it 'moves the user\'s stars from one project to another' do
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2
expect(target_project.users_star_projects.count).to eq 0
expect(target_project.star_count).to eq 0
subject.execute(project_with_stars)
project_with_stars.reload
target_project.reload
expect(project_with_stars.users_star_projects.count).to eq 0
expect(project_with_stars.star_count).to eq 0
expect(target_project.users_star_projects.count).to eq 2
expect(target_project.star_count).to eq 2
end
it 'rollbacks changes if transaction fails' do
allow(subject).to receive(:success).and_raise(StandardError)
expect { subject.execute(project_with_stars) }.to raise_error(StandardError)
expect(project_with_stars.users_star_projects.count).to eq 2
expect(project_with_stars.star_count).to eq 2
expect(target_project.users_star_projects.count).to eq 0
expect(target_project.star_count).to eq 0
end
end
end
require 'spec_helper'
describe Projects::OverwriteProjectService do
include ProjectForksHelper
let(:user) { create(:user) }
let(:project_from) { create(:project, namespace: user.namespace) }
let(:project_to) { create(:project, namespace: user.namespace) }
let!(:lvl1_forked_project_1) { fork_project(project_from, user) }
let!(:lvl1_forked_project_2) { fork_project(project_from, user) }
let!(:lvl2_forked_project_1_1) { fork_project(lvl1_forked_project_1, user) }
let!(:lvl2_forked_project_1_2) { fork_project(lvl1_forked_project_1, user) }
subject { described_class.new(project_to, user) }
before do
allow(project_to).to receive(:import_data).and_return(double(data: { 'original_path' => project_from.path }))
end
describe '#execute' do
shared_examples 'overwrite actions' do
it 'moves deploy keys' do
deploy_keys_count = project_from.deploy_keys_projects.count
subject.execute(project_from)
expect(project_to.deploy_keys_projects.count).to eq deploy_keys_count
end
it 'moves notification settings' do
notification_count = project_from.notification_settings.count
subject.execute(project_from)
expect(project_to.notification_settings.count).to eq notification_count
end
it 'moves users stars' do
stars_count = project_from.users_star_projects.count
subject.execute(project_from)
project_to.reload
expect(project_to.users_star_projects.count).to eq stars_count
expect(project_to.star_count).to eq stars_count
end
it 'moves project group links' do
group_links_count = project_from.project_group_links.count
subject.execute(project_from)
expect(project_to.project_group_links.count).to eq group_links_count
end
it 'moves memberships and authorizations' do
members_count = project_from.project_members.count
project_authorizations = project_from.project_authorizations.count
subject.execute(project_from)
expect(project_to.project_members.count).to eq members_count
expect(project_to.project_authorizations.count).to eq project_authorizations
end
context 'moves lfs objects relationships' do
before do
create_list(:lfs_objects_project, 3, project: project_from)
end
it do
lfs_objects_count = project_from.lfs_objects.count
subject.execute(project_from)
expect(project_to.lfs_objects.count).to eq lfs_objects_count
end
end
it 'removes the original project' do
subject.execute(project_from)
expect { Project.find(project_from.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'renames the project' do
subject.execute(project_from)
expect(project_to.full_path).to eq project_from.full_path
end
end
context 'when project does not have any relation' do
it_behaves_like 'overwrite actions'
end
context 'when project with elements' do
it_behaves_like 'overwrite actions' do
let(:master_user) { create(:user) }
let(:reporter_user) { create(:user) }
let(:developer_user) { create(:user) }
let(:master_group) { create(:group) }
let(:reporter_group) { create(:group) }
let(:developer_group) { create(:group) }
before do
create_list(:deploy_keys_project, 2, project: project_from)
create_list(:notification_setting, 2, source: project_from)
create_list(:users_star_project, 2, project: project_from)
project_from.project_group_links.create(group: master_group, group_access: Gitlab::Access::MASTER)
project_from.project_group_links.create(group: developer_group, group_access: Gitlab::Access::DEVELOPER)
project_from.project_group_links.create(group: reporter_group, group_access: Gitlab::Access::REPORTER)
project_from.add_master(master_user)
project_from.add_developer(developer_user)
project_from.add_reporter(reporter_user)
end
end
end
context 'forks' do
context 'when moving a root forked project' do
it 'moves the descendant forks' do
expect(project_from.forks.count).to eq 2
expect(project_to.forks.count).to eq 0
subject.execute(project_from)
expect(project_from.forks.count).to eq 0
expect(project_to.forks.count).to eq 2
expect(lvl1_forked_project_1.forked_from_project).to eq project_to
expect(lvl1_forked_project_1.fork_network_member.forked_from_project).to eq project_to
expect(lvl1_forked_project_2.forked_from_project).to eq project_to
expect(lvl1_forked_project_2.fork_network_member.forked_from_project).to eq project_to
end
it 'updates the fork network' do
expect(project_from.fork_network.root_project).to eq project_from
expect(project_from.fork_network.fork_network_members.map(&:project)).to include project_from
subject.execute(project_from)
expect(project_to.reload.fork_network.root_project).to eq project_to
expect(project_to.fork_network.fork_network_members.map(&:project)).not_to include project_from
end
end
context 'when moving a intermediate forked project' do
let(:project_to) { create(:project, namespace: lvl1_forked_project_1.namespace) }
it 'moves the descendant forks' do
expect(lvl1_forked_project_1.forks.count).to eq 2
expect(project_to.forks.count).to eq 0
subject.execute(lvl1_forked_project_1)
expect(lvl1_forked_project_1.forks.count).to eq 0
expect(project_to.forks.count).to eq 2
expect(lvl2_forked_project_1_1.forked_from_project).to eq project_to
expect(lvl2_forked_project_1_1.fork_network_member.forked_from_project).to eq project_to
expect(lvl2_forked_project_1_2.forked_from_project).to eq project_to
expect(lvl2_forked_project_1_2.fork_network_member.forked_from_project).to eq project_to
end
it 'moves the ascendant fork' do
subject.execute(lvl1_forked_project_1)
expect(project_to.reload.forked_from_project).to eq project_from
expect(project_to.fork_network_member.forked_from_project).to eq project_from
end
it 'does not update fork network' do
subject.execute(lvl1_forked_project_1)
expect(project_to.reload.fork_network.root_project).to eq project_from
end
end
end
context 'if an exception is raised' do
it 'rollbacks changes' do
updated_at = project_from.updated_at
allow(subject).to receive(:rename_project).and_raise(StandardError)
expect { subject.execute(project_from) }.to raise_error(StandardError)
expect(Project.find(project_from.id)).not_to be_nil
expect(project_from.reload.updated_at.change(usec: 0)).to eq updated_at.change(usec: 0)
end
it 'tries to restore the original project repositories' do
allow(subject).to receive(:rename_project).and_raise(StandardError)
expect(subject).to receive(:attempt_restore_repositories).with(project_from)
expect { subject.execute(project_from) }.to raise_error(StandardError)
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