diff options
author | Bob Van Landuyt <bob@gitlab.com> | 2017-04-03 15:10:30 +0200 |
---|---|---|
committer | Bob Van Landuyt <bob@gitlab.com> | 2017-05-01 11:14:23 +0200 |
commit | f76a5abb3462a4bfeacca254c0cbda4f313d4ecd (patch) | |
tree | a3e3f904f8c15282dd805bb2928d318598b666b0 | |
parent | 56e031d3032996233f2c71ba7b5e3fc398da0b53 (diff) | |
download | gitlab-ce-f76a5abb3462a4bfeacca254c0cbda4f313d4ecd.tar.gz |
Add migration to rename all namespaces with forbidden name
This is based on a migration in https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/2073
Rename forbidden child namespaces
5 files changed, 958 insertions, 0 deletions
diff --git a/db/post_migrate/20170403121055_rename_forbidden_root_namespaces.rb b/db/post_migrate/20170403121055_rename_forbidden_root_namespaces.rb new file mode 100644 index 00000000000..fb475cae465 --- /dev/null +++ b/db/post_migrate/20170403121055_rename_forbidden_root_namespaces.rb @@ -0,0 +1,247 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameForbiddenRootNamespaces < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + disable_ddl_transaction! + + class Namespace < ActiveRecord::Base + self.table_name = 'namespaces' + belongs_to :parent, class_name: "Namespace" + has_one :route, as: :source, autosave: true + has_many :children, class_name: "Namespace", foreign_key: :parent_id + has_many :projects + belongs_to :owner, class_name: "User" + + def full_path + if route && route.path.present? + @full_path ||= route.path + else + update_route if persisted? + + build_full_path + end + end + + def build_full_path + if parent && path + parent.full_path + '/' + path + else + path + end + end + + def update_route + prepare_route + route.save + end + + def prepare_route + route || build_route(source: self) + route.path = build_full_path + route.name = build_full_name + @full_path = nil + @full_name = nil + end + + def build_full_name + if parent && name + parent.human_name + ' / ' + name + else + name + end + end + + def human_name + owner&.name + end + end + + class Route < ActiveRecord::Base + self.table_name = 'routes' + belongs_to :source, polymorphic: true + + validates :source, presence: true + + validates :path, + length: { within: 1..255 }, + presence: true, + uniqueness: { case_sensitive: false } + end + + class Project < ActiveRecord::Base + self.table_name = 'projects' + + def repository_storage_path + Gitlab.config.repositories.storages[repository_storage]['path'] + end + end + + DOWNTIME = false + DISALLOWED_PATHS = %w[ + api + autocomplete + search + member + explore + uploads + import + notification_settings + abuse_reports + invites + help + koding + health_check + jwt + oauth + sent_notifications + ] + + def up + DISALLOWED_PATHS.each do |path| + say "Renaming namespaces called #{path}" + forbidden_namespaces_with_path(path).each do |namespace| + rename_namespace(namespace) + end + end + end + + def down + # nothing to do + end + + def rename_namespace(namespace) + old_path = namespace.path + old_full_path = namespace.full_path + # Only remove the last occurrence of the path name to get the parent namespace path + namespace_path = remove_last_occurrence(old_full_path, old_path) + new_path = rename_path(namespace_path, old_path) + new_full_path = if namespace_path.present? + File.join(namespace_path, new_path) + else + new_path + end + + Namespace.where(id: namespace).update_all(path: new_path) # skips callbacks & validations + + replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path) + + update_column_in_batches(:routes, :path, replace_statement) do |table, query| + query.where(Route.arel_table[:path].matches("#{old_full_path}%")) + end + + clear_cache_for_namespace(namespace) + + # tasks here are based on `Namespace#move_dir` + move_repositories(namespace, old_full_path, new_full_path) + move_namespace_folders(uploads_dir, old_full_path, new_full_path) if file_storage? + move_namespace_folders(pages_dir, old_full_path, new_full_path) + end + + # This will replace the first occurance of a string in a column with + # the replacement + # On postgresql we can use `regexp_replace` for that. + # On mysql we remove the pattern from the beginning of the string, and + # concatenate the remaining part tot the replacement. + def replace_sql(column, pattern, replacement) + if Gitlab::Database.mysql? + substr = Arel::Nodes::NamedFunction.new("substring", [column, pattern.to_s.size + 1]) + concat = Arel::Nodes::NamedFunction.new("concat", [Arel::Nodes::Quoted.new(replacement.to_s), substr]) + Arel::Nodes::SqlLiteral.new(concat.to_sql) + else + replace = Arel::Nodes::NamedFunction.new("regexp_replace", [column, Arel::Nodes::Quoted.new(pattern.to_s), Arel::Nodes::Quoted.new(replacement.to_s)]) + Arel::Nodes::SqlLiteral.new(replace.to_sql) + end + end + + def remove_last_occurrence(string, pattern) + string.reverse.sub(pattern.reverse, "").reverse + end + + def move_namespace_folders(directory, old_relative_path, new_relative_path) + old_path = File.join(directory, old_relative_path) + return unless File.directory?(old_path) + + new_path = File.join(directory, new_relative_path) + FileUtils.mv(old_path, new_path) + end + + def move_repositories(namespace, old_full_path, new_full_path) + repo_paths_for_namespace(namespace).each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, old_full_path) + + unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path) + say "Exception moving path #{repository_storage_path} from #{old_full_path} to #{new_full_path}" + end + end + end + + def rename_path(namespace_path, path_was) + counter = 0 + path = "#{path_was}#{counter}" + + while route_exists?(File.join(namespace_path, path)) + counter += 1 + path = "#{path_was}#{counter}" + end + + path + end + + def route_exists?(full_path) + Route.where(Route.arel_table[:path].matches(full_path)).any? + end + + def forbidden_namespaces_with_path(name) + Namespace.where(arel_table[:path].matches(name).and(arel_table[:parent_id].eq(nil))) + end + + def clear_cache_for_namespace(namespace) + project_ids = project_ids_for_namespace(namespace) + scopes = { "Project" => { id: project_ids }, + "Issue" => { project_id: project_ids }, + "MergeRequest" => { target_project_id: project_ids }, + "Note" => { project_id: project_ids } } + + ClearDatabaseCacheWorker.perform_async(scopes) + rescue => e + Rails.logger.error ["Couldn't clear the markdown cache: #{e.message}", e.backtrace.join("\n")].join("\n") + end + + def project_ids_for_namespace(namespace) + namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id]) + namespace_or_children = Project.arel_table[:namespace_id].in(namespace_ids) + Project.unscoped.where(namespace_or_children).pluck(:id) + end + + # This won't scale to huge trees, but it should do for a handful of namespaces + def child_ids_for_parent(namespace, ids: []) + namespace.children.each do |child| + ids << child.id + child_ids_for_parent(child, ids: ids) if child.children.any? + end + ids + end + + def repo_paths_for_namespace(namespace) + namespace.projects.unscoped.select('distinct(repository_storage)').to_a.map(&:repository_storage_path) + end + + def uploads_dir + File.join(Rails.root, "public", "uploads") + end + + def pages_dir + Settings.pages.path + end + + def file_storage? + CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File + end + + def arel_table + Namespace.arel_table + end +end diff --git a/db/post_migrate/20170404152317_rename_forbidden_child_namespaces.rb b/db/post_migrate/20170404152317_rename_forbidden_child_namespaces.rb new file mode 100644 index 00000000000..8b082a892d4 --- /dev/null +++ b/db/post_migrate/20170404152317_rename_forbidden_child_namespaces.rb @@ -0,0 +1,242 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameForbiddenChildNamespaces < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + disable_ddl_transaction! + + class Namespace < ActiveRecord::Base + self.table_name = 'namespaces' + belongs_to :parent, class_name: "Namespace" + has_one :route, as: :source, autosave: true + has_many :children, class_name: "Namespace", foreign_key: :parent_id + has_many :projects + belongs_to :owner, class_name: "User" + + def full_path + if route && route.path.present? + @full_path ||= route.path + else + update_route if persisted? + + build_full_path + end + end + + def build_full_path + if parent && path + parent.full_path + '/' + path + else + path + end + end + + def update_route + prepare_route + route.save + end + + def prepare_route + route || build_route(source: self) + route.path = build_full_path + route.name = build_full_name + @full_path = nil + @full_name = nil + end + + def build_full_name + if parent && name + parent.human_name + ' / ' + name + else + name + end + end + + def human_name + owner&.name + end + end + + class Route < ActiveRecord::Base + self.table_name = 'routes' + belongs_to :source, polymorphic: true + + validates :source, presence: true + + validates :path, + length: { within: 1..255 }, + presence: true, + uniqueness: { case_sensitive: false } + end + + class Project < ActiveRecord::Base + self.table_name = 'projects' + + def repository_storage_path + Gitlab.config.repositories.storages[repository_storage]['path'] + end + end + + DOWNTIME = false + DISALLOWED_PATHS = %w[info git-upload-pack + git-receive-pack gitlab-lfs autocomplete_sources + templates avatar commit pages compare network snippets + services mattermost deploy_keys forks import merge_requests + branches merged_branches tags protected_branches variables + triggers pipelines environments cycle_analytics builds + hooks container_registry milestones labels issues + project_members group_links notes noteable boards todos + uploads runners runner_projects settings repository + transfer remove_fork archive unarchive housekeeping + toggle_star preview_markdown export remove_export + generate_new_export download_export activity + new_issue_address] + + def up + DISALLOWED_PATHS.each do |path| + say "Renaming namespaces called #{path}" + forbidden_namespaces_with_path(path).each do |namespace| + rename_namespace(namespace) + end + end + end + + def down + # nothing to do + end + + def rename_namespace(namespace) + old_path = namespace.path + old_full_path = namespace.full_path + # Only remove the last occurrence of the path name to get the parent namespace path + namespace_path = remove_last_occurrence(old_full_path, old_path) + new_path = rename_path(namespace_path, old_path) + new_full_path = if namespace_path.present? + File.join(namespace_path, new_path) + else + new_path + end + + Namespace.where(id: namespace).update_all(path: new_path) # skips callbacks & validations + + replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path) + + update_column_in_batches(:routes, :path, replace_statement) do |table, query| + query.where(Route.arel_table[:path].matches("#{old_full_path}%")) + end + + clear_cache_for_namespace(namespace) + + # tasks here are based on `Namespace#move_dir` + move_repositories(namespace, old_full_path, new_full_path) + move_namespace_folders(uploads_dir, old_full_path, new_full_path) if file_storage? + move_namespace_folders(pages_dir, old_full_path, new_full_path) + end + + # This will replace the first occurance of a string in a column with + # the replacement + # On postgresql we can use `regexp_replace` for that. + # On mysql we remove the pattern from the beginning of the string, and + # concatenate the remaining part tot the replacement. + def replace_sql(column, pattern, replacement) + if Gitlab::Database.mysql? + substr = Arel::Nodes::NamedFunction.new("substring", [column, pattern.to_s.size + 1]) + concat = Arel::Nodes::NamedFunction.new("concat", [Arel::Nodes::Quoted.new(replacement.to_s), substr]) + Arel::Nodes::SqlLiteral.new(concat.to_sql) + else + replace = Arel::Nodes::NamedFunction.new("regexp_replace", [column, Arel::Nodes::Quoted.new(pattern.to_s), Arel::Nodes::Quoted.new(replacement.to_s)]) + Arel::Nodes::SqlLiteral.new(replace.to_sql) + end + end + + def remove_last_occurrence(string, pattern) + string.reverse.sub(pattern.reverse, "").reverse + end + + def move_namespace_folders(directory, old_relative_path, new_relative_path) + old_path = File.join(directory, old_relative_path) + return unless File.directory?(old_path) + + new_path = File.join(directory, new_relative_path) + FileUtils.mv(old_path, new_path) + end + + def move_repositories(namespace, old_full_path, new_full_path) + repo_paths_for_namespace(namespace).each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, old_full_path) + + unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path) + say "Exception moving path #{repository_storage_path} from #{old_full_path} to #{new_full_path}" + end + end + end + + def rename_path(namespace_path, path_was) + counter = 0 + path = "#{path_was}#{counter}" + + while route_exists?(File.join(namespace_path, path)) + counter += 1 + path = "#{path_was}#{counter}" + end + + path + end + + def route_exists?(full_path) + Route.where(Route.arel_table[:path].matches(full_path)).any? + end + + def forbidden_namespaces_with_path(path) + Namespace.where(arel_table[:parent_id].eq(nil).not).where(arel_table[:path].matches(path)) + end + + def clear_cache_for_namespace(namespace) + project_ids = project_ids_for_namespace(namespace) + scopes = { "Project" => { id: project_ids }, + "Issue" => { project_id: project_ids }, + "MergeRequest" => { target_project_id: project_ids }, + "Note" => { project_id: project_ids } } + + ClearDatabaseCacheWorker.perform_async(scopes) + rescue => e + Rails.logger.error ["Couldn't clear the markdown cache: #{e.message}", e.backtrace.join("\n")].join("\n") + end + + def project_ids_for_namespace(namespace) + namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id]) + namespace_or_children = Project.arel_table[:namespace_id].in(namespace_ids) + Project.unscoped.where(namespace_or_children).pluck(:id) + end + + # This won't scale to huge trees, but it should do for a handful of namespaces + def child_ids_for_parent(namespace, ids: []) + namespace.children.each do |child| + ids << child.id + child_ids_for_parent(child, ids: ids) if child.children.any? + end + ids + end + + def repo_paths_for_namespace(namespace) + namespace.projects.unscoped.select('distinct(repository_storage)').to_a.map(&:repository_storage_path) + end + + def uploads_dir + File.join(Rails.root, "public", "uploads") + end + + def pages_dir + Settings.pages.path + end + + def file_storage? + CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File + end + + def arel_table + Namespace.arel_table + end +end diff --git a/db/post_migrate/20170405111106_rename_wildcard_project_names.rb b/db/post_migrate/20170405111106_rename_wildcard_project_names.rb new file mode 100644 index 00000000000..1b8d2a40e99 --- /dev/null +++ b/db/post_migrate/20170405111106_rename_wildcard_project_names.rb @@ -0,0 +1,85 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameWildcardProjectNames < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + disable_ddl_transaction! + + DOWNTIME = false + KNOWN_PATHS = %w[info git-upload-pack + git-receive-pack gitlab-lfs autocomplete_sources + templates avatar commit pages compare network snippets + services mattermost deploy_keys forks import merge_requests + branches merged_branches tags protected_branches variables + triggers pipelines environments cycle_analytics builds + hooks container_registry milestones labels issues + project_members group_links notes noteable boards todos + uploads runners runner_projects settings repository + transfer remove_fork archive unarchive housekeeping + toggle_star preview_markdown export remove_export + generate_new_export download_export activity + new_issue_address].freeze + + def up + reserved_projects.find_in_batches(batch_size: 100) do |slice| + rename_projects(slice) + end + 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.downcase) + + ActiveRecord::Base.connection. + select_all("SELECT id, path FROM routes WHERE lower(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/spec/migrations/rename_forbidden_child_namespaces_spec.rb b/spec/migrations/rename_forbidden_child_namespaces_spec.rb new file mode 100644 index 00000000000..c5486e18052 --- /dev/null +++ b/spec/migrations/rename_forbidden_child_namespaces_spec.rb @@ -0,0 +1,187 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170404152317_rename_forbidden_child_namespaces.rb') + +describe RenameForbiddenChildNamespaces, truncate: true do + let(:migration) { described_class.new } + let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_namespaces_test') } + let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') } + let(:forbidden_namespace) do + namespace = build(:group, path: 'info') + namespace.parent = create(:group, path: 'parent') + namespace.save(validate: false) + namespace + end + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + FileUtils.remove_dir(TestEnv.repos_path) if File.directory?(TestEnv.repos_path) + allow(migration).to receive(:say) + allow(migration).to receive(:uploads_dir).and_return(uploads_dir) + end + + describe '#forbidden_namespaces_with_path' do + let(:other_namespace) { create(:group, path: 'info') } + before do + forbidden_namespace + other_namespace + end + + it 'includes namespaces called with path `info`' do + expect(migration.forbidden_namespaces_with_path('info').map(&:id)).to contain_exactly(forbidden_namespace.id) + end + end + + describe '#up' do + before do + forbidden_namespace + end + + it 'renames namespaces called info' do + migration.up + + expect(forbidden_namespace.reload.path).to eq('info0') + end + + it 'renames the route to the namespace' do + migration.up + + expect(forbidden_namespace.reload.full_path).to eq('parent/info0') + end + + it 'renames the route for projects of the namespace' do + project = create(:project, path: 'project-path', namespace: forbidden_namespace) + + migration.up + + expect(project.route.reload.path).to eq('parent/info0/project-path') + end + + it 'moves the the repository for a project in the namespace' do + create(:project, namespace: forbidden_namespace, path: 'info-project') + expected_repo = File.join(TestEnv.repos_path, 'parent/info0', 'info-project.git') + + migration.up + + expect(File.directory?(expected_repo)).to be(true) + end + + it 'moves the uploads for the namespace' do + allow(migration).to receive(:move_namespace_folders).with(Settings.pages.path, 'parent/info', 'parent/info0') + expect(migration).to receive(:move_namespace_folders).with(uploads_dir, 'parent/info', 'parent/info0') + + migration.up + end + + it 'moves the pages for the namespace' do + allow(migration).to receive(:move_namespace_folders).with(uploads_dir, 'parent/info', 'parent/info0') + expect(migration).to receive(:move_namespace_folders).with(Settings.pages.path, 'parent/info', 'parent/info0') + + migration.up + end + + it 'clears the markdown cache for projects in the forbidden namespace' do + project = create(:project, namespace: forbidden_namespace) + scopes = { 'Project' => { id: [project.id] }, + 'Issue' => { project_id: [project.id] }, + 'MergeRequest' => { target_project_id: [project.id] }, + 'Note' => { project_id: [project.id] } } + + expect(ClearDatabaseCacheWorker).to receive(:perform_async).with(scopes) + + migration.up + end + + context 'forbidden namespace -> subgroup -> info0 project' do + it 'updates the route of the project correctly' do + subgroup = create(:group, path: 'subgroup', parent: forbidden_namespace) + project = create(:project, path: 'info0', namespace: subgroup) + + migration.up + + expect(project.route.reload.path).to eq('parent/info0/subgroup/info0') + end + end + end + + describe '#move_repositories' do + let(:namespace) { create(:group, name: 'hello-group') } + it 'moves a project for a namespace' do + create(:project, namespace: namespace, path: 'hello-project') + expected_path = File.join(TestEnv.repos_path, 'bye-group', 'hello-project.git') + + migration.move_repositories(namespace, 'hello-group', 'bye-group') + + expect(File.directory?(expected_path)).to be(true) + end + + it 'moves a namespace in a subdirectory correctly' do + child_namespace = create(:group, name: 'sub-group', parent: namespace) + create(:project, namespace: child_namespace, path: 'hello-project') + + expected_path = File.join(TestEnv.repos_path, 'hello-group', 'renamed-sub-group', 'hello-project.git') + + migration.move_repositories(child_namespace, 'hello-group/sub-group', 'hello-group/renamed-sub-group') + + expect(File.directory?(expected_path)).to be(true) + end + + it 'moves a parent namespace with subdirectories' do + child_namespace = create(:group, name: 'sub-group', parent: namespace) + create(:project, namespace: child_namespace, path: 'hello-project') + expected_path = File.join(TestEnv.repos_path, 'renamed-group', 'sub-group', 'hello-project.git') + + migration.move_repositories(child_namespace, 'hello-group', 'renamed-group') + + expect(File.directory?(expected_path)).to be(true) + end + end + + describe '#move_namespace_folders' do + it 'moves a namespace with files' do + source = File.join(uploads_dir, 'parent-group', 'sub-group') + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, 'parent-group', 'moved-group') + FileUtils.touch(File.join(source, 'test.txt')) + expected_file = File.join(destination, 'test.txt') + + migration.move_namespace_folders(uploads_dir, File.join('parent-group', 'sub-group'), File.join('parent-group', 'moved-group')) + + expect(File.exist?(expected_file)).to be(true) + end + + it 'moves a parent namespace uploads' do + source = File.join(uploads_dir, 'parent-group', 'sub-group') + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, 'moved-parent', 'sub-group') + FileUtils.touch(File.join(source, 'test.txt')) + expected_file = File.join(destination, 'test.txt') + + migration.move_namespace_folders(uploads_dir, 'parent-group', 'moved-parent') + + expect(File.exist?(expected_file)).to be(true) + end + end + + describe '#child_ids_for_parent' do + it 'collects child ids for all levels' do + parent = create(:namespace) + first_child = create(:namespace, parent: parent) + second_child = create(:namespace, parent: parent) + third_child = create(:namespace, parent: second_child) + all_ids = [parent.id, first_child.id, second_child.id, third_child.id] + + collected_ids = migration.child_ids_for_parent(parent, ids: [parent.id]) + + expect(collected_ids).to contain_exactly(*all_ids) + end + end + + describe '#remove_last_ocurrence' do + it 'removes only the last occurance of a string' do + input = 'this/is/info/namespace/with/info' + + expect(migration.remove_last_occurrence(input, 'info')).to eq('this/is/info/namespace/with/') + end + end +end diff --git a/spec/migrations/rename_forbidden_root_namespaces_spec.rb b/spec/migrations/rename_forbidden_root_namespaces_spec.rb new file mode 100644 index 00000000000..ca806e08475 --- /dev/null +++ b/spec/migrations/rename_forbidden_root_namespaces_spec.rb @@ -0,0 +1,197 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170403121055_rename_forbidden_root_namespaces.rb') + +describe RenameForbiddenRootNamespaces, truncate: true do + let(:migration) { described_class.new } + let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_namespaces_test') } + let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') } + let(:forbidden_namespace) do + namespace = build(:namespace, path: 'api') + namespace.save(validate: false) + namespace + end + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + FileUtils.remove_dir(TestEnv.repos_path) if File.directory?(TestEnv.repos_path) + allow(migration).to receive(:say) + allow(migration).to receive(:uploads_dir).and_return(uploads_dir) + end + + describe '#forbidden_namespaces_with_path' do + before do + forbidden_namespace + end + + it 'includes namespaces called with path `api`' do + expect(migration.forbidden_namespaces_with_path('api').map(&:id)).to include(forbidden_namespace.id) + end + end + + describe '#up' do + before do + forbidden_namespace + end + + it 'renames namespaces called api' do + migration.up + + expect(forbidden_namespace.reload.path).to eq('api0') + end + + it 'renames the route to the namespace' do + migration.up + + expect(forbidden_namespace.reload.full_path).to eq('api0') + end + + it 'renames the route for projects of the namespace' do + project = create(:project, path: 'project-path', namespace: forbidden_namespace) + + migration.up + + expect(project.route.reload.path).to eq('api0/project-path') + end + + it 'moves the the repository for a project in the namespace' do + create(:project, namespace: forbidden_namespace, path: 'api-project') + expected_repo = File.join(TestEnv.repos_path, 'api0', 'api-project.git') + + migration.up + + expect(File.directory?(expected_repo)).to be(true) + end + + it 'moves the uploads for the namespace' do + allow(migration).to receive(:move_namespace_folders).with(Settings.pages.path, 'api', 'api0') + expect(migration).to receive(:move_namespace_folders).with(uploads_dir, 'api', 'api0') + + migration.up + end + + it 'moves the pages for the namespace' do + allow(migration).to receive(:move_namespace_folders).with(uploads_dir, 'api', 'api0') + expect(migration).to receive(:move_namespace_folders).with(Settings.pages.path, 'api', 'api0') + + migration.up + end + + it 'clears the markdown cache for projects in the forbidden namespace' do + project = create(:project, namespace: forbidden_namespace) + scopes = { 'Project' => { id: [project.id] }, + 'Issue' => { project_id: [project.id] }, + 'MergeRequest' => { target_project_id: [project.id] }, + 'Note' => { project_id: [project.id] } } + + expect(ClearDatabaseCacheWorker).to receive(:perform_async).with(scopes) + + migration.up + end + + context 'forbidden namespace -> subgroup -> api0 project' do + it 'updates the route of the project correctly' do + subgroup = create(:group, path: 'subgroup', parent: forbidden_namespace) + project = create(:project, path: 'api0', namespace: subgroup) + + migration.up + + expect(project.route.reload.path).to eq('api0/subgroup/api0') + end + end + + context 'for a sub-namespace' do + before do + forbidden_namespace.parent = create(:namespace, path: 'parent') + forbidden_namespace.save(validate: false) + end + + it "doesn't rename child-namespace paths" do + migration.up + + expect(forbidden_namespace.reload.full_path).to eq('parent/api') + end + end + end + + describe '#move_repositories' do + let(:namespace) { create(:group, name: 'hello-group') } + it 'moves a project for a namespace' do + create(:project, namespace: namespace, path: 'hello-project') + expected_path = File.join(TestEnv.repos_path, 'bye-group', 'hello-project.git') + + migration.move_repositories(namespace, 'hello-group', 'bye-group') + + expect(File.directory?(expected_path)).to be(true) + end + + it 'moves a namespace in a subdirectory correctly' do + child_namespace = create(:group, name: 'sub-group', parent: namespace) + create(:project, namespace: child_namespace, path: 'hello-project') + + expected_path = File.join(TestEnv.repos_path, 'hello-group', 'renamed-sub-group', 'hello-project.git') + + migration.move_repositories(child_namespace, 'hello-group/sub-group', 'hello-group/renamed-sub-group') + + expect(File.directory?(expected_path)).to be(true) + end + + it 'moves a parent namespace with subdirectories' do + child_namespace = create(:group, name: 'sub-group', parent: namespace) + create(:project, namespace: child_namespace, path: 'hello-project') + expected_path = File.join(TestEnv.repos_path, 'renamed-group', 'sub-group', 'hello-project.git') + + migration.move_repositories(child_namespace, 'hello-group', 'renamed-group') + + expect(File.directory?(expected_path)).to be(true) + end + end + + describe '#move_namespace_folders' do + it 'moves a namespace with files' do + source = File.join(uploads_dir, 'parent-group', 'sub-group') + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, 'parent-group', 'moved-group') + FileUtils.touch(File.join(source, 'test.txt')) + expected_file = File.join(destination, 'test.txt') + + migration.move_namespace_folders(uploads_dir, File.join('parent-group', 'sub-group'), File.join('parent-group', 'moved-group')) + + expect(File.exist?(expected_file)).to be(true) + end + + it 'moves a parent namespace uploads' do + source = File.join(uploads_dir, 'parent-group', 'sub-group') + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, 'moved-parent', 'sub-group') + FileUtils.touch(File.join(source, 'test.txt')) + expected_file = File.join(destination, 'test.txt') + + migration.move_namespace_folders(uploads_dir, 'parent-group', 'moved-parent') + + expect(File.exist?(expected_file)).to be(true) + end + end + + describe '#child_ids_for_parent' do + it 'collects child ids for all levels' do + parent = create(:namespace) + first_child = create(:namespace, parent: parent) + second_child = create(:namespace, parent: parent) + third_child = create(:namespace, parent: second_child) + all_ids = [parent.id, first_child.id, second_child.id, third_child.id] + + collected_ids = migration.child_ids_for_parent(parent, ids: [parent.id]) + + expect(collected_ids).to contain_exactly(*all_ids) + end + end + + describe '#remove_last_ocurrence' do + it 'removes only the last occurance of a string' do + input = 'this/is/api/namespace/with/api' + + expect(migration.remove_last_occurrence(input, 'api')).to eq('this/is/api/namespace/with/') + end + end +end |