1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
|
# frozen_string_literal: true
# This migration adds or updates the routes for all the entities affected by
# post-migration '20200511083541_cleanup_projects_with_missing_namespace'
# - A route is added for the 'lost-and-found' group
# - A route is added for the Ghost user (if not already defined)
# - The routes for all the orphaned projects that were moved under the 'lost-and-found'
# group are updated to reflect the new path
class UpdateRoutesForLostAndFoundGroupAndOrphanedProjects < ActiveRecord::Migration[6.0]
DOWNTIME = false
class User < ActiveRecord::Base
self.table_name = 'users'
LOST_AND_FOUND_GROUP = 'lost-and-found'
USER_TYPE_GHOST = 5
ACCESS_LEVEL_OWNER = 50
has_one :namespace, -> { where(type: nil) },
foreign_key: :owner_id, inverse_of: :owner, autosave: true,
class_name: 'UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace'
def lost_and_found_group
# Find the 'lost-and-found' group
# There should only be one Group owned by the Ghost user starting with 'lost-and-found'
Group
.joins('INNER JOIN members ON namespaces.id = members.source_id')
.where('namespaces.type = ?', 'Group')
.where('members.type = ?', 'GroupMember')
.where('members.source_type = ?', 'Namespace')
.where('members.user_id = ?', self.id)
.where('members.access_level = ?', ACCESS_LEVEL_OWNER)
.find_by(Group.arel_table[:name].matches("#{LOST_AND_FOUND_GROUP}%"))
end
class << self
# Return the ghost user
def ghost
User.find_by(user_type: USER_TYPE_GHOST)
end
end
end
# Temporary Concern to not repeat the same methods twice
module HasPath
extend ActiveSupport::Concern
def full_path
if parent && path
parent.full_path + '/' + path
else
path
end
end
def full_name
if parent && name
parent.full_name + ' / ' + name
else
name
end
end
end
class Namespace < ActiveRecord::Base
include HasPath
self.table_name = 'namespaces'
belongs_to :owner, class_name: 'UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::User'
belongs_to :parent, class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace"
has_many :children, foreign_key: :parent_id,
class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace"
has_many :projects, class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Project"
def ensure_route!
unless Route.for_source('Namespace', self.id)
Route.create!(
source_id: self.id,
source_type: 'Namespace',
path: self.full_path,
name: self.full_name
)
end
end
def generate_unique_path
# Generate a unique path if there is no route for the namespace
# (an existing route guarantees that the path is already unique)
unless Route.for_source('Namespace', self.id)
self.path = Uniquify.new.string(self.path) do |str|
Route.where(path: str).exists?
end
end
end
end
class Group < Namespace
# Disable STI to allow us to manually set "type = 'Group'"
# Otherwise rails forces "type = CleanupProjectsWithMissingNamespace::Group"
self.inheritance_column = :_type_disabled
end
class Route < ActiveRecord::Base
self.table_name = 'routes'
def self.for_source(source_type, source_id)
Route.find_by(source_type: source_type, source_id: source_id)
end
end
class Project < ActiveRecord::Base
include HasPath
self.table_name = 'projects'
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id',
class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Group"
belongs_to :namespace,
class_name: "UpdateRoutesForLostAndFoundGroupAndOrphanedProjects::Namespace"
alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id
def ensure_route!
Route.find_or_initialize_by(source_type: 'Project', source_id: self.id).tap do |record|
record.path = self.full_path
record.name = self.full_name
record.save!
end
end
end
def up
# Reset the column information of all the models that update the database
# to ensure the Active Record's knowledge of the table structure is current
Namespace.reset_column_information
Route.reset_column_information
# Find the ghost user, its namespace and the "lost and found" group
ghost_user = User.ghost
return unless ghost_user # No reason to continue if there is no Ghost user
ghost_namespace = ghost_user.namespace
lost_and_found_group = ghost_user.lost_and_found_group
# No reason to continue if there is no 'lost-and-found' group
# 1. No orphaned projects were found in this instance, or
# 2. The 'lost-and-found' group and the orphaned projects have been already deleted
return unless lost_and_found_group
# Update the 'lost-and-found' group description to be more self-explanatory
lost_and_found_group.generate_unique_path
lost_and_found_group.description =
'Group for storing projects that were not properly deleted. '\
'It should be considered as a system level Group with non-working '\
'projects inside it. The contents may be deleted with a future update. '\
'More info: gitlab.com/gitlab-org/gitlab/-/issues/198603'
lost_and_found_group.save!
# Update the routes for the Ghost user, the "lost and found" group
# and all the orphaned projects
ghost_namespace.ensure_route!
lost_and_found_group.ensure_route!
# The following does a fast index scan by namespace_id
# No reason to process in batches:
# - 66 projects in GitLab.com, less than 1ms execution time to fetch them
# with a constant update time for each
lost_and_found_group.projects.each do |project|
project.ensure_route!
end
end
def down
# no-op
end
end
|