Commit 27f54beb authored by Bob Van Landuyt's avatar Bob Van Landuyt

Use objects for renaming namespaces and projects

parent 0369ef14
module Gitlab module Gitlab
module Database module Database
module RenameReservedPathsMigration module RenameReservedPathsMigration
include MigrationHelpers def self.included(kls)
include Namespaces kls.include(MigrationHelpers)
include Projects end
def rename_wildcard_paths(one_or_more_paths) def rename_wildcard_paths(one_or_more_paths)
paths = Array(one_or_more_paths) paths = Array(one_or_more_paths)
rename_namespaces(paths, type: :wildcard) RenameNamespaces.new(paths, self).rename_namespaces(type: :wildcard)
rename_projects(paths) RenameProjects.new(paths, self).rename_projects
end end
def rename_root_paths(paths) def rename_root_paths(paths)
paths = Array(paths) paths = Array(paths)
rename_namespaces(paths, type: :top_level) RenameNamespaces.new(paths, self).rename_namespaces(type: :top_level)
end
def rename_path_for_routable(routable)
old_path = routable.path
old_full_path = routable.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 = join_routable_path(namespace_path, new_path)
# skips callbacks & validations
routable.class.where(id: routable).
update_all(path: new_path)
rename_routes(old_full_path, new_full_path)
[old_full_path, new_full_path]
end
def rename_routes(old_full_path, new_full_path)
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(MigrationClasses::Route.arel_table[:path].matches("#{old_full_path}%"))
end
end
def rename_path(namespace_path, path_was)
counter = 0
path = "#{path_was}#{counter}"
while route_exists?(join_routable_path(namespace_path, path))
counter += 1
path = "#{path_was}#{counter}"
end
path
end
def remove_last_occurrence(string, pattern)
string.reverse.sub(pattern.reverse, "").reverse
end
def join_routable_path(namespace_path, top_level)
if namespace_path.present?
File.join(namespace_path, top_level)
else
top_level
end
end
def route_exists?(full_path)
MigrationClasses::Route.where(Route.arel_table[:path].matches(full_path)).any?
end
def move_pages(old_path, new_path)
move_folders(pages_dir, old_path, new_path)
end
def move_uploads(old_path, new_path)
return unless file_storage?
move_folders(uploads_dir, old_path, new_path)
end
def move_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 file_storage?
CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
end
def uploads_dir
File.join(CarrierWave.root, "uploads")
end
def pages_dir
Settings.pages.path
end end
end end
end end
......
...@@ -76,6 +76,7 @@ module Gitlab ...@@ -76,6 +76,7 @@ module Gitlab
class Project < ActiveRecord::Base class Project < ActiveRecord::Base
include MigrationClasses::Routable include MigrationClasses::Routable
has_one :route, as: :source
self.table_name = 'projects' self.table_name = 'projects'
def repository_storage_path def repository_storage_path
......
module Gitlab
module Database
module RenameReservedPathsMigration
class RenameBase
attr_reader :paths, :migration
delegate :update_column_in_batches,
:replace_sql,
to: :migration
def initialize(paths, migration)
@paths = paths
@migration = migration
end
def rename_path_for_routable(routable)
old_path = routable.path
old_full_path = routable.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 = join_routable_path(namespace_path, new_path)
# skips callbacks & validations
routable.class.where(id: routable).
update_all(path: new_path)
rename_routes(old_full_path, new_full_path)
[old_full_path, new_full_path]
end
def rename_routes(old_full_path, new_full_path)
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(MigrationClasses::Route.arel_table[:path].matches("#{old_full_path}%"))
end
end
def rename_path(namespace_path, path_was)
counter = 0
path = "#{path_was}#{counter}"
while route_exists?(join_routable_path(namespace_path, path))
counter += 1
path = "#{path_was}#{counter}"
end
path
end
def remove_last_occurrence(string, pattern)
string.reverse.sub(pattern.reverse, "").reverse
end
def join_routable_path(namespace_path, top_level)
if namespace_path.present?
File.join(namespace_path, top_level)
else
top_level
end
end
def route_exists?(full_path)
MigrationClasses::Route.where(Route.arel_table[:path].matches(full_path)).any?
end
def move_pages(old_path, new_path)
move_folders(pages_dir, old_path, new_path)
end
def move_uploads(old_path, new_path)
return unless file_storage?
move_folders(uploads_dir, old_path, new_path)
end
def move_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 file_storage?
CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
end
def uploads_dir
File.join(CarrierWave.root, "uploads")
end
def pages_dir
Settings.pages.path
end
end
end
end
end
module Gitlab module Gitlab
module Database module Database
module RenameReservedPathsMigration module RenameReservedPathsMigration
module Namespaces class RenameNamespaces < RenameBase
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
def rename_namespaces(paths, type:) def rename_namespaces(type:)
namespaces_for_paths(paths, type: type).each do |namespace| namespaces_for_paths(type: type).each do |namespace|
rename_namespace(namespace) rename_namespace(namespace)
end end
end end
def namespaces_for_paths(paths, type:) def namespaces_for_paths(type:)
namespaces = if type == :wildcard namespaces = if type == :wildcard
MigrationClasses::Namespace.where.not(parent_id: nil) MigrationClasses::Namespace.where.not(parent_id: nil)
elsif type == :top_level elsif type == :top_level
...@@ -52,7 +52,7 @@ module Gitlab ...@@ -52,7 +52,7 @@ module Gitlab
namespace_or_children = MigrationClasses::Project. namespace_or_children = MigrationClasses::Project.
arel_table[:namespace_id]. arel_table[:namespace_id].
in(namespace_ids) in(namespace_ids)
MigrationClasses::Project.unscoped.where(namespace_or_children) MigrationClasses::Project.where(namespace_or_children)
end end
# This won't scale to huge trees, but it should do for a handful of # This won't scale to huge trees, but it should do for a handful of
......
module Gitlab module Gitlab
module Database module Database
module RenameReservedPathsMigration module RenameReservedPathsMigration
module Projects class RenameProjects < RenameBase
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
def rename_projects(paths) def rename_projects
projects_for_paths(paths).each do |project| projects_for_paths.each do |project|
rename_project(project) rename_project(project)
end end
end end
...@@ -27,7 +27,7 @@ module Gitlab ...@@ -27,7 +27,7 @@ module Gitlab
end end
end end
def projects_for_paths(paths) def projects_for_paths
with_paths = MigrationClasses::Project.arel_table[:path] with_paths = MigrationClasses::Project.arel_table[:path]
.matches_any(paths) .matches_any(paths)
MigrationClasses::Project.where(with_paths) MigrationClasses::Project.where(with_paths)
......
require 'spec_helper'
describe Gitlab::Database::RenameReservedPathsMigration::RenameBase do
let(:migration) { FakeRenameReservedPathMigration.new }
let(:subject) { described_class.new(['the-path'], migration) }
before do
allow(migration).to receive(:say)
end
def migration_namespace(namespace)
Gitlab::Database::RenameReservedPathsMigration::MigrationClasses::
Namespace.find(namespace.id)
end
def migration_project(project)
Gitlab::Database::RenameReservedPathsMigration::MigrationClasses::
Project.find(project.id)
end
describe "#remove_last_ocurrence" do
it "removes only the last occurance of a string" do
input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace"
expect(subject.remove_last_occurrence(input, "a-word-to-replace"))
.to eq("this/is/a-word-to-replace/namespace/with/")
end
end
describe '#rename_path_for_routable' do
context 'for namespaces' do
let(:namespace) { create(:namespace, path: 'the-path') }
it "renames namespaces called the-path" do
subject.rename_path_for_routable(migration_namespace(namespace))
expect(namespace.reload.path).to eq("the-path0")
end
it "renames the route to the namespace" do
subject.rename_path_for_routable(migration_namespace(namespace))
expect(Namespace.find(namespace.id).full_path).to eq("the-path0")
end
it "renames the route for projects of the namespace" do
project = create(:project, path: "project-path", namespace: namespace)
subject.rename_path_for_routable(migration_namespace(namespace))
expect(project.route.reload.path).to eq("the-path0/project-path")
end
it 'returns the old & the new path' do
old_path, new_path = subject.rename_path_for_routable(migration_namespace(namespace))
expect(old_path).to eq('the-path')
expect(new_path).to eq('the-path0')
end
context "the-path namespace -> subgroup -> the-path0 project" do
it "updates the route of the project correctly" do
subgroup = create(:group, path: "subgroup", parent: namespace)
project = create(:project, path: "the-path0", namespace: subgroup)
subject.rename_path_for_routable(migration_namespace(namespace))
expect(project.route.reload.path).to eq("the-path0/subgroup/the-path0")
end
end
end
context 'for projects' do
let(:parent) { create(:namespace, path: 'the-parent') }
let(:project) { create(:empty_project, path: 'the-path', namespace: parent) }
it 'renames the project called `the-path`' do
subject.rename_path_for_routable(migration_project(project))
expect(project.reload.path).to eq('the-path0')
end
it 'renames the route for the project' do
subject.rename_path_for_routable(project)
expect(project.reload.route.path).to eq('the-parent/the-path0')
end
it 'returns the old & new path' do
old_path, new_path = subject.rename_path_for_routable(migration_project(project))
expect(old_path).to eq('the-parent/the-path')
expect(new_path).to eq('the-parent/the-path0')
end
end
end
describe '#move_pages' do
it 'moves the pages directory' do
expect(subject).to receive(:move_folders)
.with(TestEnv.pages_path, 'old-path', 'new-path')
subject.move_pages('old-path', 'new-path')
end
end
describe "#move_uploads" do
let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') }
let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') }
it 'moves subdirectories in the uploads folder' do
expect(subject).to receive(:uploads_dir).and_return(uploads_dir)
expect(subject).to receive(:move_folders).with(uploads_dir, 'old_path', 'new_path')
subject.move_uploads('old_path', 'new_path')
end
it "doesn't move uploads when they are stored in object storage" do
expect(subject).to receive(:file_storage?).and_return(false)
expect(subject).not_to receive(:move_folders)
subject.move_uploads('old_path', 'new_path')
end
end
describe '#move_folders' do
let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') }
let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') }
before do
FileUtils.remove_dir(test_dir) if File.directory?(test_dir)
FileUtils.mkdir_p(uploads_dir)
allow(subject).to receive(:uploads_dir).and_return(uploads_dir)
end
it 'moves a folder 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')
subject.move_folders(uploads_dir, File.join('parent-group', 'sub-group'), File.join('parent-group', 'moved-group'))
expect(File.exist?(expected_file)).to be(true)
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Database::RenameReservedPathsMigration::Namespaces, :truncate do describe Gitlab::Database::RenameReservedPathsMigration::RenameNamespaces do
let(:subject) do let(:migration) { FakeRenameReservedPathMigration.new }
ActiveRecord::Migration.new.extend( let(:subject) { described_class.new(['the-path'], migration) }
Gitlab::Database::RenameReservedPathsMigration
)
end
before do before do
allow(subject).to receive(:say) allow(migration).to receive(:say)
end end
def migration_namespace(namespace) def migration_namespace(namespace)
...@@ -27,7 +24,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::Namespaces, :truncate d ...@@ -27,7 +24,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::Namespaces, :truncate d
path: 'the-path', path: 'the-path',
parent: create(:namespace)) parent: create(:namespace))
found_ids = subject.namespaces_for_paths(['the-PATH'], type: :wildcard). found_ids = subject.namespaces_for_paths(type: :wildcard).
map(&:id) map(&:id)
expect(found_ids).to contain_exactly(namespace.id) expect(found_ids).to contain_exactly(namespace.id)
end end
...@@ -41,7 +38,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::Namespaces, :truncate d ...@@ -41,7 +38,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::Namespaces, :truncate d
path: 'the-path', path: 'the-path',
parent: create(:namespace)) parent: create(:namespace))
found_ids = subject.namespaces_for_paths(['the-path'], type: :top_level). found_ids = subject.namespaces_for_paths(type: :top_level).
map(&:id) map(&:id)
expect(found_ids).to contain_exactly(root_namespace.id) expect(found_ids).to contain_exactly(root_namespace.id)
end end
...@@ -98,12 +95,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::Namespaces, :truncate d ...@@ -98,12 +95,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::Namespaces, :truncate d
describe "#rename_namespace" do describe "#rename_namespace" do
let(:namespace) { create(:namespace, path: 'the-path') } let(:namespace) { create(:namespace, path: 'the-path') }
it 'renames paths & routes for the namesapce' do it 'renames paths & routes for the namespace' do
expect(subject).to receive(:rename_path_for_routable). expect(subject).to receive(:rename_path_for_routable).
with(namespace). with(namespace).
and_call_original and_call_original
subject.rename_namespace(namespace) subject.rename_namespace(namespace)
expect(namespace.reload.path).to eq('the-path0')
end end
it "moves the the repository for a project in the namespace" do it "moves the the repository for a project in the namespace" do
...@@ -140,14 +139,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::Namespaces, :truncate d ...@@ -140,14 +139,14 @@ describe Gitlab::Database::RenameReservedPathsMigration::Namespaces, :truncate d
expect(subject).to receive(:rename_namespace). expect(subject).to receive(:rename_namespace).
with(migration_namespace(top_level_namespace)) with(migration_namespace(top_level_namespace))
subject.rename_namespaces(['the-path'], type: :top_level) subject.rename_namespaces(type: :top_level)
end end
it 'renames child namespaces' do it 'renames child namespaces' do
expect(subject).to receive(:rename_namespace). expect(subject).to receive(:rename_namespace).
with(migration_namespace(child_namespace)) with(migration_namespace(child_namespace))
subject.rename_namespaces(['the-path'], type: :wildcard) subject.rename_namespaces(type: :wildcard)
end end
end end
end end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Database::RenameReservedPathsMigration::Projects, :truncate do describe Gitlab::Database::RenameReservedPathsMigration::RenameProjects do
let(:subject) do let(:migration) { FakeRenameReservedPathMigration.new }
ActiveRecord::Migration.new.extend( let(:subject) { described_class.new(['the-path'], migration) }
Gitlab::Database::RenameReservedPathsMigration
)
end
before do before do
allow(subject).to receive(:say) allow(migration).to receive(:say)
end end
describe '#projects_for_paths' do describe '#projects_for_paths' do
...@@ -16,7 +13,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::Projects, :truncate do ...@@ -16,7 +13,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::Projects, :truncate do
project = create(:empty_project, path: 'THE-path') project = create(:empty_project, path: 'THE-path')
_other_project = create(:empty_project) _other_project = create(:empty_project)
result_ids = subject.projects_for_paths(['the-PATH']).map(&:id) result_ids = subject.projects_for_paths.map(&:id)
expect(result_ids).to contain_exactly(project.id) expect(result_ids).to contain_exactly(project.id)
end end
...@@ -35,6 +32,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::Projects, :truncate do ...@@ -35,6 +32,8 @@ describe Gitlab::Database::RenameReservedPathsMigration::Projects, :truncate do
and_call_original and_call_original
subject.rename_project(project) subject.rename_project(project)
expect(project.reload.path).to eq('the-path0')
end end
it 'moves the wiki & the repo' do it 'moves the wiki & the repo' do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Database::RenameReservedPathsMigration do describe Gitlab::Database::RenameReservedPathsMigration do
let(:subject) do let(:subject) { FakeRenameReservedPathMigration.new }
ActiveRecord::Migration.new.extend(
Gitlab::Database::RenameReservedPathsMigration
)
end
before do before do
allow(subject).to receive(:say) allow(subject).to receive(:say)
...@@ -13,14 +9,23 @@ describe Gitlab::Database::RenameReservedPathsMigration do ...@@ -13,14 +9,23 @@ describe Gitlab::Database::RenameReservedPathsMigration do
describe '#rename_wildcard_paths' do describe '#rename_wildcard_paths' do
it 'should rename namespaces' do it 'should rename namespaces' do
expect(subject).to receive(:rename_namespaces). rename_namespaces = double
with(['first-path', 'second-path'], type: :wildcard) expect(Gitlab::Database::RenameReservedPathsMigration::RenameNamespaces).
to receive(:new).with(['first-path', 'second-path'], subject).
and_return(rename_namespaces)
expect(rename_namespaces).to receive(:rename_namespaces).
with(type: :wildcard)
subject.rename_wildcard_paths(['first-path', 'second-path']) subject.rename_wildcard_paths(['first-path', 'second-path'])
end end
it 'should rename projects' do it 'should rename projects' do
expect(subject).to receive(:rename_projects).with(['the-path']) rename_projects = double
expect(Gitlab::Database::RenameReservedPathsMigration::RenameProjects).
to receive(:new).with(['the-path'], subject).
and_return(rename_projects)
expect(rename_projects).to receive(:rename_projects)
subject.rename_wildcard_paths(['the-path']) subject.rename_wildcard_paths(['the-path'])
end end
...@@ -28,128 +33,14 @@ describe Gitlab::Database::RenameReservedPathsMigration do ...@@ -28,128 +33,14 @@ describe Gitlab::Database::RenameReservedPathsMigration do
describe '#rename_root_paths' do describe '#rename_root_paths' do
it 'should rename namespaces' do it 'should rename namespaces' do
expect(subject).to receive(:rename_namespaces). rename_namespaces = double
with(['the-path'], type: :top_level) expect(Gitlab::Database::RenameReservedPathsMigration::RenameNamespaces).
to receive(:new).with(['the-path'], subject).
and_return(rename_namespaces)
expect(rename_namespaces).to receive(:rename_namespaces).
with(type: :top_level)
subject.rename_root_paths('the-path') subject.rename_root_paths('the-path')
end end
end end
describe "#remove_last_ocurrence" do
it "removes only the last occurance of a string" do
input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace"
expect(subject.remove_last_occurrence(input, "a-word-to-replace"))
.to eq("this/is/a-word-to-replace/namespace/with/")
end
end
describe '#rename_path_for_routable' do
context 'for namespaces' do
let(:namespace) { create(:namespace, path: 'the-path') }
it "renames namespaces called the-path" do
subject.rename_path_for_routable(namespace)
expect(namespace.reload.path).to eq("the-path0")
end
it "renames the route to the namespace" do
subject.rename_path_for_routable(namespace)
expect(Namespace.find(namespace.id).full_path).to eq("the-path0")
end
it "renames the route for projects of the namespace" do
project = create(:project, path: "project-path", namespace: namespace)
subject.rename_path_for_routable(namespace)
expect(project.route.reload.path).to eq("the-path0/project-path")
end
it 'returns the old & the new path' do
old_path, new_path = subject.rename_path_for_routable(namespace)
expect(old_path).to eq('the-path')
expect(new_path).to eq('the-path0')
end
context "the-path namespace -> subgroup -> the-path0 project" do
it "updates the route of the project correctly" do
subgroup = create(:group, path: "subgroup", parent: namespace)
project = create(:project, path: "the-path0", namespace: subgroup)
subject.rename_path_for_routable(namespace)
expect(project.route.reload.path).to eq("the-path0/subgroup/the-path0")
end
end
end
context 'for projects' do
let(:parent) { create(:namespace, path: 'the-parent') }
let(:project) { create(:empty_project, path: 'the-path', namespace: parent) }
it 'renames the project called `the-path`' do
subject.rename_path_for_routable(project)
expect(project.reload.path).to eq('the-path0')
end
it 'renames the route for the project' do
subject.rename_path_for_routable(project)
expect(project.reload.route.path).to eq('the-parent/the-path0')
end
it 'returns the old & new path' do
old_path, new_path = subject.rename_path_for_routable(project)
expect(old_path).to eq('the-parent/the-path')
expect(new_path).to eq('the-parent/the-path0')
end
end
end
describe "#move_uploads" do
let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') }
let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') }
it 'moves subdirectories in the uploads folder' do
expect(subject).to receive(:uploads_dir).and_return(uploads_dir)
expect(subject).to receive(:move_folders).with(uploads_dir, 'old_path', 'new_path')
subject.move_uploads('old_path', 'new_path')
end
it "doesn't move uploads when they are stored in object storage" do
expect(subject).to receive(:file_storage?).and_return(false)
expect(subject).not_to receive(:move_folders)
subject.move_uploads('old_path', 'new_path')
end
end
describe '#move_folders' do
let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') }
let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') }
before do
FileUtils.remove_dir(test_dir) if File.directory?(test_dir)
FileUtils.mkdir_p(uploads_dir)
allow(subject).to receive(:uploads_dir).and_return(uploads_dir)
end
it 'moves a folder 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')
subject.move_folders(uploads_dir, File.join('parent-group', 'sub-group'), File.join('parent-group', 'moved-group'))
expect(File.exist?(expected_file)).to be(true)
end
end
end end
class FakeRenameReservedPathMigration < ActiveRecord::Migration
include Gitlab::Database::RenameReservedPathsMigration
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment