summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Edwards-Jones <jedwardsjones@gitlab.com>2017-06-02 14:44:59 +0100
committerJames Edwards-Jones <jedwardsjones@gitlab.com>2017-06-20 17:35:33 +0100
commit6db1a28016899170e947b045c2769639b479d72a (patch)
tree15d7f2d2c2cd8844cbcb0dc96ae43ed18f837ae5
parentf07aee72bef4604312e11a43fce3a47865bce100 (diff)
downloadgitlab-ce-30213-project-transfer-move-rollback.tar.gz
Rollback project folder move after error in Projects::TransferService30213-project-transfer-move-rollback
-rw-r--r--app/services/projects/transfer_service.rb114
-rw-r--r--changelogs/unreleased/30213-project-transfer-move-rollback.yml4
-rw-r--r--spec/services/projects/transfer_service_spec.rb61
3 files changed, 139 insertions, 40 deletions
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 1c24b27a870..fd701e33524 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -12,87 +12,121 @@ module Projects
TransferError = Class.new(StandardError)
def execute(new_namespace)
- if new_namespace.blank?
+ @new_namespace = new_namespace
+
+ if @new_namespace.blank?
raise TransferError, 'Please select a new namespace for your project.'
end
- unless allowed_transfer?(current_user, project, new_namespace)
+
+ unless allowed_transfer?(current_user, project)
raise TransferError, 'Transfer failed, please contact an admin.'
end
- transfer(project, new_namespace)
+
+ transfer(project)
+
+ true
rescue Projects::TransferService::TransferError => ex
project.reload
project.errors.add(:new_namespace, ex.message)
false
end
- def transfer(project, new_namespace)
- old_namespace = project.namespace
+ private
- Project.transaction do
- old_path = project.path_with_namespace
- old_group = project.group
- new_path = File.join(new_namespace.try(:full_path) || '', project.path)
+ def transfer(project)
+ @old_path = project.path_with_namespace
+ @old_group = project.group
+ @new_path = File.join(@new_namespace.try(:full_path) || '', project.path)
+ @old_namespace = project.namespace
- if Project.where(path: project.path, namespace_id: new_namespace.try(:id)).present?
- raise TransferError.new("Project with same path in target namespace already exists")
- end
+ if Project.where(path: project.path, namespace_id: @new_namespace.try(:id)).exists?
+ raise TransferError.new("Project with same path in target namespace already exists")
+ end
- if project.has_container_registry_tags?
- # we currently doesn't support renaming repository if it contains tags in container registry
- raise TransferError.new('Project cannot be transferred, because tags are present in its container registry')
- end
+ if project.has_container_registry_tags?
+ # We currently don't support renaming repository if it contains tags in container registry
+ raise TransferError.new('Project cannot be transferred, because tags are present in its container registry')
+ end
- project.expire_caches_before_rename(old_path)
+ attempt_transfer_transaction
+ end
+
+ def attempt_transfer_transaction
+ Project.transaction do
+ project.expire_caches_before_rename(@old_path)
- # Apply new namespace id and visibility level
- project.namespace = new_namespace
- project.visibility_level = new_namespace.visibility_level unless project.visibility_level_allowed_by_group?
- project.save!
+ update_namespace_and_visibility(@new_namespace)
# Notifications
- project.send_move_instructions(old_path)
+ project.send_move_instructions(@old_path)
# Move main repository
- unless gitlab_shell.mv_repository(project.repository_storage_path, old_path, new_path)
+ unless move_repo_folder(@old_path, @new_path)
raise TransferError.new('Cannot move project')
end
# Move wiki repo also if present
- gitlab_shell.mv_repository(project.repository_storage_path, "#{old_path}.wiki", "#{new_path}.wiki")
+ move_repo_folder("#{@old_path}.wiki", "#{@new_path}.wiki")
# Move missing group labels to project
- Labels::TransferService.new(current_user, old_group, project).execute
+ Labels::TransferService.new(current_user, @old_group, project).execute
# Move uploads
- Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path)
+ Gitlab::UploadsTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
# Move pages
- Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.full_path, new_namespace.full_path)
+ Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path)
- project.old_path_with_namespace = old_path
+ project.old_path_with_namespace = @old_path
- SystemHooksService.new.execute_hooks_for(project, :transfer)
+ execute_system_hooks
end
-
- refresh_permissions(old_namespace, new_namespace)
-
- true
+ rescue Exception # rubocop:disable Lint/RescueException
+ rollback_side_effects
+ raise
+ ensure
+ refresh_permissions
end
- def allowed_transfer?(current_user, project, namespace)
- namespace &&
+ def allowed_transfer?(current_user, project)
+ @new_namespace &&
can?(current_user, :change_namespace, project) &&
- namespace.id != project.namespace_id &&
- current_user.can?(:create_projects, namespace)
+ @new_namespace.id != project.namespace_id &&
+ current_user.can?(:create_projects, @new_namespace)
end
- def refresh_permissions(old_namespace, new_namespace)
+ def update_namespace_and_visibility(to_namespace)
+ # Apply new namespace id and visibility level
+ project.namespace = to_namespace
+ project.visibility_level = to_namespace.visibility_level unless project.visibility_level_allowed_by_group?
+ project.save!
+ end
+
+ def refresh_permissions
# This ensures we only schedule 1 job for every user that has access to
# the namespaces.
- user_ids = old_namespace.user_ids_for_project_authorizations |
- new_namespace.user_ids_for_project_authorizations
+ user_ids = @old_namespace.user_ids_for_project_authorizations |
+ @new_namespace.user_ids_for_project_authorizations
UserProjectAccessChangedService.new(user_ids).execute
end
+
+ def rollback_side_effects
+ rollback_folder_move
+ update_namespace_and_visibility(@old_namespace)
+ end
+
+ def rollback_folder_move
+ move_repo_folder(@new_path, @old_path)
+ move_repo_folder("#{@new_path}.wiki", "#{@old_path}.wiki")
+ end
+
+ def move_repo_folder(from_name, to_name)
+ gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name)
+ end
+
+ def execute_system_hooks
+ SystemHooksService.new.execute_hooks_for(project, :transfer)
+ end
end
end
diff --git a/changelogs/unreleased/30213-project-transfer-move-rollback.yml b/changelogs/unreleased/30213-project-transfer-move-rollback.yml
new file mode 100644
index 00000000000..3eb1e399c54
--- /dev/null
+++ b/changelogs/unreleased/30213-project-transfer-move-rollback.yml
@@ -0,0 +1,4 @@
+---
+title: Rollback project repo move if there is an error in Projects::TransferService
+merge_request: 11877
+author:
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index b957517c715..feb14b0e2bc 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -19,6 +19,67 @@ describe Projects::TransferService, services: true do
it { expect(project.namespace).to eq(group) }
end
+ context 'when transfer succeeds' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'sends notifications' do
+ expect_any_instance_of(NotificationService).to receive(:project_was_moved)
+
+ transfer_project(project, user, group)
+ end
+
+ it 'executes system hooks' do
+ expect_any_instance_of(Projects::TransferService).to receive(:execute_system_hooks)
+
+ transfer_project(project, user, group)
+ end
+ end
+
+ context 'when transfer fails' do
+ let!(:original_path) { project_path(project) }
+
+ def attempt_project_transfer
+ expect do
+ transfer_project(project, user, group)
+ end.to raise_error(ActiveRecord::ActiveRecordError)
+ end
+
+ before do
+ group.add_owner(user)
+
+ expect_any_instance_of(Labels::TransferService).to receive(:execute).and_raise(ActiveRecord::StatementInvalid, "PG ERROR")
+ end
+
+ def project_path(project)
+ File.join(project.repository_storage_path, "#{project.path_with_namespace}.git")
+ end
+
+ def current_path
+ project_path(project)
+ end
+
+ it 'rolls back repo location' do
+ attempt_project_transfer
+
+ expect(Dir.exist?(original_path)).to be_truthy
+ expect(original_path).to eq current_path
+ end
+
+ it "doesn't send move notifications" do
+ expect_any_instance_of(NotificationService).not_to receive(:project_was_moved)
+
+ attempt_project_transfer
+ end
+
+ it "doesn't run system hooks" do
+ expect_any_instance_of(Projects::TransferService).not_to receive(:execute_system_hooks)
+
+ attempt_project_transfer
+ end
+ end
+
context 'namespace -> no namespace' do
before do
@result = transfer_project(project, user, nil)