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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'open3'
require 'fileutils'
require 'uri'
class SchemaRegenerator
##
# Filename of the schema
#
# This file is being regenerated by this script.
FILENAME = 'db/structure.sql'
##
# Directories where migrations are stored
#
# The methods +hide_migrations+ and +unhide_migrations+ will rename
# these to disable/enable migrations.
MIGRATION_DIRS = %w[db/migrate db/post_migrate].freeze
##
# Directory where we store schema versions
#
# The remove_schema_migration_files removes files added in this
# directory when it runs.
SCHEMA_MIGRATIONS_DIR = 'db/schema_migrations/'
def execute
Dir.chdir(File.expand_path('..', __dir__)) do
checkout_ref
checkout_clean_schema
hide_migrations
remove_schema_migration_files
reset_db
unhide_migrations
migrate
ensure
unhide_migrations
end
end
private
##
# Git checkout +CI_COMMIT_SHA+.
#
# When running from CI, checkout the clean commit,
# not the merged result.
def checkout_ref
return unless ci?
run %Q[git checkout #{source_ref}]
run %q[git clean -f -- db]
end
##
# Checkout the clean schema from the target branch
def checkout_clean_schema
remote_checkout_clean_schema || local_checkout_clean_schema
end
##
# Get clean schema from remote servers
#
# This script might run in CI, using a shallow clone, so to checkout
# the file, fetch the target branch from the server.
def remote_checkout_clean_schema
return false unless project_url
return false unless target_project_url
run %Q[git remote add target_project #{target_project_url}.git]
run %Q[git fetch target_project #{target_branch}:#{target_branch}]
local_checkout_clean_schema
end
##
# Git checkout the schema from target branch.
#
# Ask git to checkout the schema from the target branch and reset
# the file to unstage the changes.
def local_checkout_clean_schema
run %Q[git checkout #{merge_base} -- #{FILENAME}]
run %Q[git reset -- #{FILENAME}]
end
##
# Move migrations to where Rails will not find them.
#
# To reset the database to clean schema defined in +FILENAME+, move
# the migrations to a path where Rails will not find them, otherwise
# +db:reset+ would abort. Later when the migrations should be
# applied, use +unhide_migrations+ to bring them back.
def hide_migrations
MIGRATION_DIRS.each do |dir|
File.rename(dir, "#{dir}__")
end
end
##
# Undo the effect of +hide_migrations+.
#
# Place back the migrations which might be moved by
# +hide_migrations+.
def unhide_migrations
error = nil
MIGRATION_DIRS.each do |dir|
File.rename("#{dir}__", dir)
rescue Errno::ENOENT
nil
rescue StandardError => e
# Save error for later, but continue with other dirs first
error = e
end
raise error if error
end
##
# Remove files added to db/schema_migrations
#
# In order to properly reset the database and re-run migrations
# the schema migrations for new migrations must be removed.
def remove_schema_migration_files
(untracked_schema_migrations + commited_schema_migrations).each do |schema_migration|
FileUtils.rm(schema_migration)
end
end
##
# List of untracked schema migrations
#
# Get a list of schema migrations that are not tracked so we can remove them
def untracked_schema_migrations
git_command = "git ls-files --others --exclude-standard -- #{SCHEMA_MIGRATIONS_DIR}"
run(git_command).chomp.split("\n")
end
##
# List of untracked schema migrations
#
# Get a list of schema migrations that have been committed since the last
def commited_schema_migrations
git_command = "git diff --name-only --diff-filter=A #{merge_base} -- #{SCHEMA_MIGRATIONS_DIR}"
run(git_command).chomp.split("\n")
end
##
# Run rake task to reset the database.
def reset_db
run %q[bin/rails db:reset RAILS_ENV=test]
end
##
# Run rake task to run migrations.
def migrate
run %q[bin/rails db:migrate RAILS_ENV=test]
end
##
# Run the given +cmd+.
#
# The command is colored green, and the output of the command is
# colored gray.
# When the command failed an exception is raised.
def run(cmd)
puts "\e[32m$ #{cmd}\e[37m"
stdout_str, stderr_str, status = Open3.capture3(cmd)
puts "#{stdout_str}#{stderr_str}\e[0m"
raise("Command failed: #{stderr_str}") unless status.success?
stdout_str
end
##
# Return the base commit between source and target branch.
def merge_base
@merge_base ||= run("git merge-base #{target_branch} #{source_ref}").chomp
end
##
# Return the name of the target branch
#
# Get source ref from CI environment variable, or read the +TARGET+
# environment+ variable, or default to +HEAD+.
def target_branch
ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'] || ENV['TARGET'] || 'master'
end
##
# Return the source ref
#
# Get source ref from CI environment variable, or default to +HEAD+.
def source_ref
ENV['CI_COMMIT_SHA'] || 'HEAD'
end
##
# Return the source project URL from CI environment variable.
def project_url
ENV['CI_PROJECT_URL']
end
##
# Return the target project URL from CI environment variable.
def target_project_url
ENV['CI_MERGE_REQUEST_PROJECT_URL']
end
##
# Return whether the script is running from CI
def ci?
ENV['CI']
end
end
SchemaRegenerator.new.execute
|