summaryrefslogtreecommitdiff
path: root/scripts/regenerate-schema
blob: 06230942dcd3960c3b4821f6b8deb6c74b824534 (plain)
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
221
222
223
224
225
226
227
#!/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
      stop_spring
      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 + committed_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 committed_schema_migrations
    git_command = "git diff --name-only --diff-filter=A #{merge_base} -- #{SCHEMA_MIGRATIONS_DIR}"
    run(git_command).chomp.split("\n")
  end

  ##
  # Stop spring before modifying the database
  def stop_spring
    run %q[bin/spring stop]
  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'] || ENV['CI_DEFAULT_BRANCH'] || '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