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
This commit is contained in:
commit
0fff9db5ea
|
@ -286,6 +286,10 @@ class Group < Namespace
|
|||
false
|
||||
end
|
||||
|
||||
def refresh_project_authorizations
|
||||
refresh_members_authorized_projects(blocking: false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_two_factor_requirement
|
||||
|
|
|
@ -252,6 +252,10 @@ class Namespace < ActiveRecord::Base
|
|||
[]
|
||||
end
|
||||
|
||||
def refresh_project_authorizations
|
||||
owner.refresh_authorized_projects
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def path_or_parent_changed?
|
||||
|
|
|
@ -1472,7 +1472,9 @@ class Project < ActiveRecord::Base
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
self.old_path_with_namespace = full_path_was
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
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
|
|||
raise
|
||||
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
|
||||
|
||||
def repo_path
|
||||
|
@ -70,12 +84,9 @@ module Projects
|
|||
# Skip repository removal. We use this flag when remove user or group
|
||||
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)
|
||||
|
||||
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}\"")
|
||||
|
||||
project.run_after_commit do
|
||||
|
@ -87,6 +98,13 @@ module Projects
|
|||
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)
|
||||
return unless project
|
||||
|
||||
|
|
|
@ -15,9 +15,18 @@ module Projects
|
|||
file = params.delete(:file)
|
||||
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_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
|
||||
end
|
||||
|
@ -31,5 +40,17 @@ module Projects
|
|||
def tmp_filename
|
||||
SecureRandom.hex
|
||||
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
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
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
|
|
@ -0,0 +1,31 @@
|
|||
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
|
|
@ -0,0 +1,42 @@
|
|||
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
|
|
@ -0,0 +1,29 @@
|
|||
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
|
|
@ -0,0 +1,38 @@
|
|||
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
|
|
@ -0,0 +1,40 @@
|
|||
# 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
|
|
@ -0,0 +1,40 @@
|
|||
# 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
|
|
@ -0,0 +1,40 @@
|
|||
# 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
|
|
@ -0,0 +1,20 @@
|
|||
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
|
|
@ -0,0 +1,69 @@
|
|||
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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Extend API for importing a project export with overwrite support
|
||||
merge_request: 17883
|
||||
author:
|
||||
type: added
|
|
@ -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 |
|
||||
| `file` | string | yes | The file to be uploaded |
|
||||
| `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)] |
|
||||
|
||||
The override params passed will take precendence over all values defined inside the export file.
|
||||
|
|
|
@ -26,6 +26,7 @@ module API
|
|||
requires :path, type: String, desc: 'The new project path and name'
|
||||
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 :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,
|
||||
type: Hash,
|
||||
desc: 'New project params to override values in the export' do
|
||||
|
@ -50,7 +51,8 @@ module API
|
|||
project_params = {
|
||||
path: import_params[:path],
|
||||
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)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
module Gitlab
|
||||
module ImportExport
|
||||
class Importer
|
||||
include Gitlab::Allowable
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
def self.imports_repository?
|
||||
true
|
||||
end
|
||||
|
@ -13,12 +16,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
def execute
|
||||
if import_file && check_version! && restorers.all?(&:restore)
|
||||
if import_file && check_version! && restorers.all?(&:restore) && overwrite_project
|
||||
project_tree.restored_project
|
||||
else
|
||||
raise Projects::ImportService::Error.new(@shared.errors.join(', '))
|
||||
end
|
||||
|
||||
rescue => e
|
||||
raise Projects::ImportService::Error.new(e.message)
|
||||
ensure
|
||||
remove_import_file
|
||||
end
|
||||
|
||||
|
@ -26,7 +31,7 @@ module Gitlab
|
|||
|
||||
def restorers
|
||||
[repo_restorer, wiki_restorer, project_tree, avatar_restorer,
|
||||
uploads_restorer, lfs_restorer]
|
||||
uploads_restorer, lfs_restorer, statistics_restorer]
|
||||
end
|
||||
|
||||
def import_file
|
||||
|
@ -69,6 +74,10 @@ module Gitlab
|
|||
Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: @shared)
|
||||
end
|
||||
|
||||
def statistics_restorer
|
||||
Gitlab::ImportExport::StatisticsRestorer.new(project: project_tree.restored_project, shared: @shared)
|
||||
end
|
||||
|
||||
def path_with_namespace
|
||||
File.join(@project.namespace.full_path, @project.path)
|
||||
end
|
||||
|
@ -84,6 +93,33 @@ module Gitlab
|
|||
def remove_import_file
|
||||
FileUtils.rm_rf(@archive_file)
|
||||
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
|
||||
|
|
|
@ -92,7 +92,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
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|
|
||||
Project.column_names.include?(key.to_s) &&
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
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
|
|
@ -0,0 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :users_star_project do
|
||||
project
|
||||
user
|
||||
end
|
||||
end
|
|
@ -1,6 +1,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ImportExport::Importer do
|
||||
let(:user) { create(:user) }
|
||||
let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
|
||||
let(:shared) { project.import_export_shared }
|
||||
let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) }
|
||||
|
@ -11,6 +12,7 @@ describe Gitlab::ImportExport::Importer do
|
|||
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
|
||||
FileUtils.mkdir_p(shared.export_path)
|
||||
FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path)
|
||||
allow(subject).to receive(:remove_import_file)
|
||||
end
|
||||
|
||||
after do
|
||||
|
@ -42,7 +44,8 @@ describe Gitlab::ImportExport::Importer do
|
|||
Gitlab::ImportExport::RepoRestorer,
|
||||
Gitlab::ImportExport::WikiRestorer,
|
||||
Gitlab::ImportExport::UploadsRestorer,
|
||||
Gitlab::ImportExport::LfsRestorer
|
||||
Gitlab::ImportExport::LfsRestorer,
|
||||
Gitlab::ImportExport::StatisticsRestorer
|
||||
].each do |restorer|
|
||||
it "calls the #{restorer}" do
|
||||
fake_restorer = double(restorer.to_s)
|
||||
|
@ -60,5 +63,42 @@ describe Gitlab::ImportExport::Importer do
|
|||
importer.execute
|
||||
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
|
||||
|
|
|
@ -114,6 +114,29 @@ describe API::ProjectImport do
|
|||
expect(import_project.description).to eq('Hello world')
|
||||
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)
|
||||
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
|
||||
|
|
|
@ -248,6 +248,28 @@ describe Projects::DestroyService do
|
|||
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 = {})
|
||||
if async
|
||||
Projects::DestroyService.new(project, user, params).async_execute
|
||||
|
|
|
@ -4,7 +4,8 @@ describe Projects::GitlabProjectsImportService do
|
|||
set(:namespace) { create(:namespace) }
|
||||
let(:path) { 'test-path' }
|
||||
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) }
|
||||
|
||||
describe '#execute' do
|
||||
|
@ -37,5 +38,28 @@ describe Projects::GitlabProjectsImportService do
|
|||
expect(project.import_data.data['override_params']['description']).to eq('Hello')
|
||||
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
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
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
|
|
@ -0,0 +1,58 @@
|
|||
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
|
|
@ -0,0 +1,96 @@
|
|||
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
|
|
@ -0,0 +1,55 @@
|
|||
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
|
|
@ -0,0 +1,56 @@
|
|||
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
|
|
@ -0,0 +1,56 @@
|
|||
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
|
|
@ -0,0 +1,65 @@
|
|||
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
|
|
@ -0,0 +1,65 @@
|
|||
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
|
|
@ -0,0 +1,42 @@
|
|||
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
|
|
@ -0,0 +1,198 @@
|
|||
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
|
Loading…
Reference in New Issue