summaryrefslogtreecommitdiff
path: root/db/migrate
diff options
context:
space:
mode:
authorYorick Peterse <yorickpeterse@gmail.com>2017-05-03 18:08:27 +0200
committerYorick Peterse <yorickpeterse@gmail.com>2017-05-17 20:53:22 +0200
commit53250d6d8a7f016e8a7f503509e489b444859429 (patch)
tree9c8f017674dc4df4a55d44e4c8f9941d791cfd93 /db/migrate
parent34974258bc3f745c86512319231bad47232fe007 (diff)
downloadgitlab-ce-53250d6d8a7f016e8a7f503509e489b444859429.tar.gz
Convert nested groups to regular ones for MySQL
This migration will take all nested groups and convert them into regular groups, ensuring that members of any parent groups still have access to the child group. This migration relies on code external to it as copying all of this over involves hundreds of lines of code depending on all sorts of methods, making this practically impossible to do right.
Diffstat (limited to 'db/migrate')
-rw-r--r--db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb123
1 files changed, 123 insertions, 0 deletions
diff --git a/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb
new file mode 100644
index 00000000000..c67690642c9
--- /dev/null
+++ b/db/migrate/20170503140202_turn_nested_groups_into_regular_groups_for_mysql.rb
@@ -0,0 +1,123 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+# This migration depends on code external to it. For example, it relies on
+# updating a namespace to also rename directories (uploads, GitLab pages, etc).
+# The alternative is to copy hundreds of lines of code into this migration,
+# adjust them where needed, etc; something which doesn't work well at all.
+class TurnNestedGroupsIntoRegularGroupsForMysql < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def run_migration?
+ Gitlab::Database.mysql?
+ end
+
+ def up
+ return unless run_migration?
+
+ # For all sub-groups we need to give the right people access. We do this as
+ # follows:
+ #
+ # 1. Get all the ancestors for the current namespace
+ # 2. Get all the members of these namespaces, along with their higher access
+ # level
+ # 3. Give these members access to the current namespace
+ Namespace.unscoped.where('parent_id IS NOT NULL').find_each do |namespace|
+ rows = []
+ existing = namespace.members.pluck(:user_id)
+
+ all_members_for(namespace).each do |member|
+ next if existing.include?(member[:user_id])
+
+ rows << {
+ access_level: member[:access_level],
+ source_id: namespace.id,
+ source_type: 'Namespace',
+ user_id: member[:user_id],
+ notification_level: 3, # global
+ type: 'GroupMember',
+ created_at: Time.current,
+ updated_at: Time.current
+ }
+ end
+
+ bulk_insert_members(rows)
+
+ # This method relies on the parent to determine the proper path.
+ # Because we reset "parent_id" this method will not return the right path
+ # when moving namespaces.
+ full_path_was = namespace.send(:full_path_was)
+
+ namespace.define_singleton_method(:full_path_was) { full_path_was }
+
+ namespace.update!(parent_id: nil, path: new_path_for(namespace))
+ end
+ end
+
+ def down
+ # There is no way to go back from regular groups to nested groups.
+ end
+
+ # Generates a new (unique) path for a namespace.
+ def new_path_for(namespace)
+ counter = 1
+ base = namespace.full_path.tr('/', '-')
+ new_path = base
+
+ while Namespace.unscoped.where(path: new_path).exists?
+ new_path = base + "-#{counter}"
+ counter += 1
+ end
+
+ new_path
+ end
+
+ # Returns an Array containing all the ancestors of the current namespace.
+ #
+ # This method is not particularly efficient, but it's probably still faster
+ # than using the "routes" table. Most importantly of all, it _only_ depends
+ # on the namespaces table and the "parent_id" column.
+ def ancestors_for(namespace)
+ ancestors = []
+ current = namespace
+
+ while current&.parent_id
+ # We're using find_by(id: ...) here to deal with cases where the
+ # parent_id may point to a missing row.
+ current = Namespace.unscoped.select([:id, :parent_id]).
+ find_by(id: current.parent_id)
+
+ ancestors << current.id if current
+ end
+
+ ancestors
+ end
+
+ # Returns a relation containing all the members that have access to any of
+ # the current namespace's parent namespaces.
+ def all_members_for(namespace)
+ Member.
+ unscoped.
+ select(['user_id', 'MAX(access_level) AS access_level']).
+ where(source_type: 'Namespace', source_id: ancestors_for(namespace)).
+ group(:user_id)
+ end
+
+ def bulk_insert_members(rows)
+ return if rows.empty?
+
+ keys = rows.first.keys
+
+ tuples = rows.map do |row|
+ row.map { |(_, value)| connection.quote(value) }
+ end
+
+ execute <<-EOF.strip_heredoc
+ INSERT INTO members (#{keys.join(', ')})
+ VALUES #{tuples.map { |tuple| "(#{tuple.join(', ')})" }.join(', ')}
+ EOF
+ end
+end