#!/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