diff --git a/app/models/project.rb b/app/models/project.rb index 19bbe65b01d6b6281eae3dc5b770e5393e463aea..e0ffa7e7af7f6a2e8cfc31cfc534006cbdfffd8a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -926,7 +926,7 @@ def rename_repo Rails.logger.error "Project #{old_path_with_namespace} cannot be renamed because container registry tags are present" # we currently doesn't support renaming repository if it contains tags in container registry - raise Exception.new('Project cannot be renamed, because tags are present in its container registry') + raise StandardError.new('Project cannot be renamed, because tags are present in its container registry') end if gitlab_shell.mv_repository(repository_storage_path, old_path_with_namespace, new_path_with_namespace) @@ -953,7 +953,7 @@ def rename_repo # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs - raise Exception.new('repository cannot be renamed') + raise StandardError.new('repository cannot be renamed') end Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" diff --git a/changelogs/unreleased/dz-rename-reserved-project-names.yml b/changelogs/unreleased/dz-rename-reserved-project-names.yml new file mode 100644 index 0000000000000000000000000000000000000000..30bcc1a0396c4bb358d367161895b80a6bec23c9 --- /dev/null +++ b/changelogs/unreleased/dz-rename-reserved-project-names.yml @@ -0,0 +1,4 @@ +--- +title: Rename projects wth reserved names +merge_request: 8234 +author: diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb new file mode 100644 index 0000000000000000000000000000000000000000..7f7c2424a5cad6bda842748903432003024039fd --- /dev/null +++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb @@ -0,0 +1,135 @@ +require 'thread' + +class RenameReservedProjectNames < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + + DOWNTIME = false + + THREAD_COUNT = 8 + + KNOWN_PATHS = %w(.well-known + all + assets + blame + blob + commits + create + create_dir + edit + files + files + find_file + groups + hooks + issues + logs_tree + merge_requests + new + new + preview + profile + projects + public + raw + repository + robots.txt + s + snippets + teams + tree + u + unsubscribes + update + users + wikis) + + def up + queues = Array.new(THREAD_COUNT) { Queue.new } + start = false + + threads = Array.new(THREAD_COUNT) do |index| + Thread.new do + queue = queues[index] + + # Wait until we have input to process. + until start; end + + rename_projects(queue.pop) until queue.empty? + end + end + + enum = queues.each + + reserved_projects.each_slice(100) do |slice| + begin + queue = enum.next + rescue StopIteration + enum.rewind + retry + end + + queue << slice + end + + start = true + + threads.each(&:join) + end + + def down + # nothing to do here + end + + private + + def reserved_projects + Project.unscoped. + includes(:namespace). + where('EXISTS (SELECT 1 FROM namespaces WHERE projects.namespace_id = namespaces.id)'). + where('projects.path' => KNOWN_PATHS) + end + + def route_exists?(full_path) + quoted_path = ActiveRecord::Base.connection.quote_string(full_path) + + ActiveRecord::Base.connection. + select_all("SELECT id, path FROM routes WHERE path = '#{quoted_path}'").present? + end + + # Adds number to the end of the path that is not taken by other route + def rename_path(namespace_path, path_was) + counter = 0 + path = "#{path_was}#{counter}" + + while route_exists?("#{namespace_path}/#{path}") + counter += 1 + path = "#{path_was}#{counter}" + end + + path + end + + def rename_projects(projects) + projects.each do |project| + id = project.id + path_was = project.path + namespace_path = project.namespace.path + path = rename_path(namespace_path, path_was) + + begin + # Because project path update is quite complex operation we can't safely + # copy-paste all code from GitLab. As exception we use Rails code here + project.rename_repo if rename_project_row(project, path) + rescue Exception => e # rubocop: disable Lint/RescueException + Rails.logger.error "Exception when renaming project #{id}: #{e.message}" + end + end + end + + def rename_project_row(project, path) + project.respond_to?(:update_attributes) && + project.update_attributes(path: path) && + project.respond_to?(:rename_repo) + end +end diff --git a/db/schema.rb b/db/schema.rb index 2198dcee2cda857e062cef218849a82381795138..e1e72bdae8f40a424425d4c59adef0b1b6d639e6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1306,4 +1306,4 @@ add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" -end +end \ No newline at end of file diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..4fb7ed36884ad6bce04de27c323bd5ae97b52fcc --- /dev/null +++ b/spec/migrations/rename_reserved_project_names_spec.rb @@ -0,0 +1,47 @@ +# encoding: utf-8 + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20161221153951_rename_reserved_project_names.rb') + +# This migration uses multiple threads, and thus different transactions. This +# means data created in this spec may not be visible to some threads. To work +# around this we use the TRUNCATE cleaning strategy. +describe RenameReservedProjectNames, truncate: true do + let(:migration) { described_class.new } + let!(:project) { create(:empty_project) } + + before do + project.path = 'projects' + project.save!(validate: false) + end + + describe '#up' do + context 'when project repository exists' do + before { project.create_repository } + + context 'when no exception is raised' do + it 'renames project with reserved names' do + migration.up + + expect(project.reload.path).to eq('projects0') + end + end + + context 'when exception is raised during rename' do + before do + allow(project).to receive(:rename_repo).and_raise(StandardError) + end + + it 'captures exception from project rename' do + expect { migration.up }.not_to raise_error + end + end + end + + context 'when project repository does not exist' do + it 'does not raise error' do + expect { migration.up }.not_to raise_error + end + end + end +end