From 82e6ed310b1bb5e7faf44742defaf65b74926195 Mon Sep 17 00:00:00 2001
From: Bob Van Landuyt <bob@vanlanduyt.co>
Date: Fri, 28 Jun 2019 20:00:52 +0200
Subject: [PATCH] Fix incorrect namespaces & route for user-routes

This fixes the `Namespace#name` and `Route#name` for all user
namespaces and their personal projects in case they don't match the
user name anymore.

More info info in https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23272
---
 ...chedule_fixing_names_of_user_namespaces.rb |  48 ++++++++
 .../fix_user_namespace_names.rb               |  68 ++++++++++++
 .../fix_user_project_route_names.rb           |  38 +++++++
 .../fix_user_namespace_names_spec.rb          | 104 ++++++++++++++++++
 .../fix_user_project_route_names_spec.rb      |  98 +++++++++++++++++
 5 files changed, 356 insertions(+)
 create mode 100644 db/post_migrate/20190628191740_schedule_fixing_names_of_user_namespaces.rb
 create mode 100644 lib/gitlab/background_migration/fix_user_namespace_names.rb
 create mode 100644 lib/gitlab/background_migration/fix_user_project_route_names.rb
 create mode 100644 spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb
 create mode 100644 spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb

diff --git a/db/post_migrate/20190628191740_schedule_fixing_names_of_user_namespaces.rb b/db/post_migrate/20190628191740_schedule_fixing_names_of_user_namespaces.rb
new file mode 100644
index 00000000000..8fa7068b957
--- /dev/null
+++ b/db/post_migrate/20190628191740_schedule_fixing_names_of_user_namespaces.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ScheduleFixingNamesOfUserNamespaces < ActiveRecord::Migration[5.1]
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  class Namespace < ActiveRecord::Base
+    include ::EachBatch
+
+    self.table_name = 'namespaces'
+
+    scope :user_namespaces, -> { where(type: nil) }
+  end
+
+  class Route < ActiveRecord::Base
+    include ::EachBatch
+
+    self.table_name = 'routes'
+
+    scope :project_routes, -> { where(source_type: 'Project') }
+  end
+
+  disable_ddl_transaction!
+
+  def up
+    queue_background_migration_jobs_by_range_at_intervals(
+      ScheduleFixingNamesOfUserNamespaces::Namespace.user_namespaces,
+      'FixUserNamespaceNames',
+      60.seconds,
+      batch_size: 5000
+    )
+
+    queue_background_migration_jobs_by_range_at_intervals(
+      ScheduleFixingNamesOfUserNamespaces::Route.project_routes,
+      'FixUserProjectRouteNames',
+      60.seconds,
+      batch_size: 5000
+    )
+  end
+
+  def down
+    # no-op
+  end
+end
diff --git a/lib/gitlab/background_migration/fix_user_namespace_names.rb b/lib/gitlab/background_migration/fix_user_namespace_names.rb
new file mode 100644
index 00000000000..1a207121be0
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_user_namespace_names.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module BackgroundMigration
+    # This migration fixes the namespaces.name for all user-namespaces that have names
+    # that aren't equal to the users name.
+    # Then it uses the updated names of the namespaces to update the associated routes
+    # For more info see https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23272
+    class FixUserNamespaceNames
+      def perform(from_id, to_id)
+        fix_namespace_names(from_id, to_id)
+        fix_namespace_route_names(from_id, to_id)
+      end
+
+      def fix_namespace_names(from_id, to_id)
+        ActiveRecord::Base.connection.execute <<~UPDATE_NAMESPACES
+          WITH namespaces_to_update AS (
+              SELECT
+                  namespaces.id,
+                  users.name AS correct_name
+              FROM
+                  namespaces
+                  INNER JOIN users ON namespaces.owner_id = users.id
+              WHERE
+                  namespaces.type IS NULL
+                  AND namespaces.id BETWEEN #{from_id} AND #{to_id}
+                  AND namespaces.name != users.name
+          )
+          UPDATE
+              namespaces
+          SET
+              name = correct_name
+          FROM
+              namespaces_to_update
+          WHERE
+              namespaces.id = namespaces_to_update.id
+        UPDATE_NAMESPACES
+      end
+
+      def fix_namespace_route_names(from_id, to_id)
+        ActiveRecord::Base.connection.execute <<~ROUTES_UPDATE
+          WITH routes_to_update AS (
+              SELECT
+                  routes.id,
+                  users.name AS correct_name
+              FROM
+                  routes
+                  INNER JOIN namespaces ON routes.source_id = namespaces.id
+                  INNER JOIN users ON namespaces.owner_id = users.id
+              WHERE
+                  namespaces.type IS NULL
+                  AND routes.source_type = 'Namespace'
+                  AND namespaces.id BETWEEN #{from_id} AND #{to_id}
+                  AND (routes.name != users.name OR routes.name IS NULL)
+          )
+          UPDATE
+              routes
+          SET
+              name = correct_name
+          FROM
+              routes_to_update
+          WHERE
+              routes_to_update.id = routes.id
+        ROUTES_UPDATE
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/background_migration/fix_user_project_route_names.rb b/lib/gitlab/background_migration/fix_user_project_route_names.rb
new file mode 100644
index 00000000000..b84ff32e712
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_user_project_route_names.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Gitlab
+  module BackgroundMigration
+    # This migration fixes the routes.name for all user-projects that have names
+    # that don't start with the users name.
+    # For more info see https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23272
+    class FixUserProjectRouteNames
+      def perform(from_id, to_id)
+        ActiveRecord::Base.connection.execute <<~ROUTES_UPDATE
+          WITH routes_to_update AS (
+            SELECT
+                routes.id,
+                users.name || ' / ' || projects.name AS correct_name
+            FROM
+                routes
+                INNER JOIN projects ON routes.source_id = projects.id
+                INNER JOIN namespaces ON projects.namespace_id = namespaces.id
+                INNER JOIN users ON namespaces.owner_id = users.id
+            WHERE
+                routes.source_type = 'Project'
+                AND routes.id BETWEEN #{from_id} AND #{to_id}
+                AND namespaces.type IS NULL
+                AND (routes.name NOT LIKE users.name || '%' OR routes.name IS NULL)
+          )
+          UPDATE
+              routes
+          SET
+              name = routes_to_update.correct_name
+          FROM
+              routes_to_update
+          WHERE
+              routes_to_update.id = routes.id
+        ROUTES_UPDATE
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb b/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb
new file mode 100644
index 00000000000..5938ecca459
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/fix_user_namespace_names_spec.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::FixUserNamespaceNames, :migration, schema: 20190620112608 do
+  let(:namespaces) { table(:namespaces) }
+  let(:users) { table(:users) }
+  let(:user) { users.create(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') }
+
+  context 'updating the namespace names' do
+    it 'updates a user namespace within range' do
+      user2 = users.create(name: "Other user's full name", projects_limit: 10, username: 'also-not-null', email: '2')
+      user_namespace1 = namespaces.create(
+        id: 2,
+        owner_id: user.id,
+        name: "Should be the user's name",
+        path: user.username
+      )
+      user_namespace2 = namespaces.create(
+        id: 3,
+        owner_id: user2.id,
+        name: "Should also be the user's name",
+        path: user.username
+      )
+
+      described_class.new.perform(1, 5)
+
+      expect(user_namespace1.reload.name).to eq("The user's full name")
+      expect(user_namespace2.reload.name).to eq("Other user's full name")
+    end
+
+    it 'does not update namespaces out of range' do
+      user_namespace = namespaces.create(
+        id: 6,
+        owner_id: user.id,
+        name: "Should be the user's name",
+        path: user.username
+      )
+
+      expect { described_class.new.perform(1, 5) }
+        .not_to change { user_namespace.reload.name }
+    end
+
+    it 'does not update groups owned by the users' do
+      user_group = namespaces.create(
+        id: 2,
+        owner_id: user.id,
+        name: 'A group name',
+        path: 'the-path',
+        type: 'Group'
+      )
+
+      expect { described_class.new.perform(1, 5) }
+        .not_to change { user_group.reload.name }
+    end
+  end
+
+  context 'namespace route names' do
+    let(:routes) { table(:routes) }
+    let(:namespace) do
+      namespaces.create(
+        id: 2,
+        owner_id: user.id,
+        name: "Will be updated to the user's name",
+        path: user.username
+      )
+    end
+
+    it "updates the route name if it didn't match the namespace" do
+      route = routes.create(path: namespace.path, name: 'Incorrect name', source_type: 'Namespace', source_id: namespace.id)
+
+      described_class.new.perform(1, 5)
+
+      expect(route.reload.name).to eq("The user's full name")
+    end
+
+    it 'updates the route name if it was nil match the namespace' do
+      route = routes.create(path: namespace.path, name: nil, source_type: 'Namespace', source_id: namespace.id)
+
+      described_class.new.perform(1, 5)
+
+      expect(route.reload.name).to eq("The user's full name")
+    end
+
+    it "doesn't update group routes" do
+      route = routes.create(path: 'group-path', name: 'Group name', source_type: 'Group', source_id: namespace.id)
+
+      expect { described_class.new.perform(1, 5) }
+        .not_to change { route.reload.name }
+    end
+
+    it "doesn't touch routes for namespaces out of range" do
+      user_namespace = namespaces.create(
+        id: 6,
+        owner_id: user.id,
+        name: "Should be the user's name",
+        path: user.username
+      )
+
+      expect { described_class.new.perform(1, 5) }
+        .not_to change { user_namespace.reload.name }
+    end
+  end
+end
diff --git a/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb b/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb
new file mode 100644
index 00000000000..d1d6d8411d1
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/fix_user_project_route_names_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::FixUserProjectRouteNames, :migration, schema: 20190620112608 do
+  let(:namespaces) { table(:namespaces) }
+  let(:users) { table(:users) }
+  let(:routes) { table(:routes) }
+  let(:projects) { table(:projects) }
+
+  let(:user) { users.create(name: "The user's full name", projects_limit: 10, username: 'not-null', email: '1') }
+
+  let(:namespace) do
+    namespaces.create(
+      owner_id: user.id,
+      name: "Should eventually be the user's name",
+      path: user.username
+    )
+  end
+
+  let(:project) do
+    projects.create(namespace_id: namespace.id, name: 'Project Name')
+  end
+
+  it "updates the route for a project if it did not match the user's name" do
+    route = routes.create(
+      id: 1,
+      path: "#{user.username}/#{project.path}",
+      source_id: project.id,
+      source_type: 'Project',
+      name: 'Completely wrong'
+    )
+
+    described_class.new.perform(1, 5)
+
+    expect(route.reload.name).to eq("The user's full name / Project Name")
+  end
+
+  it 'updates the route for a project if the name was nil' do
+    route = routes.create(
+      id: 1,
+      path: "#{user.username}/#{project.path}",
+      source_id: project.id,
+      source_type: 'Project',
+      name: nil
+    )
+
+    described_class.new.perform(1, 5)
+
+    expect(route.reload.name).to eq("The user's full name / Project Name")
+  end
+
+  it 'does not update routes that were are out of the range' do
+    route = routes.create(
+      id: 6,
+      path: "#{user.username}/#{project.path}",
+      source_id: project.id,
+      source_type: 'Project',
+      name: 'Completely wrong'
+    )
+
+    expect { described_class.new.perform(1, 5) }
+      .not_to change { route.reload.name }
+  end
+
+  it 'does not update routes for projects in groups owned by the user' do
+    group = namespaces.create(
+      owner_id: user.id,
+      name: 'A group',
+      path: 'a-path',
+      type: ''
+    )
+    project = projects.create(namespace_id: group.id, name: 'Project Name')
+    route = routes.create(
+      id: 1,
+      path: "#{group.path}/#{project.path}",
+      source_id: project.id,
+      source_type: 'Project',
+      name: 'Completely wrong'
+    )
+
+    expect { described_class.new.perform(1, 5) }
+      .not_to change { route.reload.name }
+  end
+
+  it 'does not update routes for namespaces' do
+    route = routes.create(
+      id: 1,
+      path: namespace.path,
+      source_id: namespace.id,
+      source_type: 'Namespace',
+      name: 'Completely wrong'
+    )
+
+    expect { described_class.new.perform(1, 5) }
+      .not_to change { route.reload.name }
+  end
+end
-- 
2.30.9