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
|
#!/usr/bin/env ruby
# frozen_string_literal: true
# This script is designed to manage database migrations for GitLab.
# It allows users to selectively run or revert migrations that have been changed in the current Git branch.
# This is especially useful for database reviewers and maintainers
# The script uses the 'fzf' command-line fuzzy finder for interactive selection of migration files.
#
# Examples:
#
# 1. Running migrations:
# $ ./scripts/database/migrate.rb
# This will show a list of changed migration files and allow you to select which ones to apply.
#
# 2. Reverting migrations:
# $ ./scripts/database/migrate.rb -t down
# This will show a list of changed migration files and allow you to select which ones to revert.
#
# 3. Debug mode:
# $ ruby scripts/database/migrate.rb --debug
# This will run the script with additional debug output for troubleshooting.
#
# The script checks for changed migration files in both 'db/migrate' and 'db/post_migrate' directories,
# and executes the selected migrations for both the main and CI databases.
require 'optparse'
require 'set'
SCRIPT_NAME = File.basename($PROGRAM_NAME)
MIGRATIONS_DIR = 'db/migrate'
POST_DEPLOY_MIGRATIONS_DIR = 'db/post_migrate'
BRANCH_NAME = 'master'
def require_commands!(*commands)
missing_commands = commands.reject { |command| system("command", "-v", command, out: File::NULL) }
abort("This script requires #{missing_commands.join(', ')} to be installed.") unless missing_commands.empty?
end
def parse_options
options = {
task: :up
}
OptionParser.new do |opts|
opts.banner = "Usage: #{SCRIPT_NAME} [options]"
opts.on('--debug', 'Enable debug mode') do |v|
options[:debug] = v
end
opts.on('-t', '--task=TASK', 'Set task') do |v|
options[:task] = v
end
end.parse!
options
end
def prompt(list, prompt:, multi: false, reverse: false)
arr = list.join("\n")
fzf_args = [].tap do |args|
args << '--layout="reverse"'
args << '--height=30%'
args << '--multi' if multi
args << '--tac' if reverse
end
output = IO.popen("echo \"#{arr}\" | fzf #{fzf_args.join(' ')} --prompt=\"#{prompt}\"", &:readlines)
return [] unless output
selection = output.join.strip
return selection unless multi
selection.split("\n")
end
def get_changed_files(branch_name:)
set = `git diff --name-only --diff-filter=d $(git merge-base #{branch_name} HEAD)..HEAD #{MIGRATIONS_DIR}`
.split("\n").to_set
set += `git diff --diff-filter=d --merge-base --name-only #{branch_name} #{MIGRATIONS_DIR}`.split("\n")
set
end
# rubocop:disable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity -- we can skip it for this script
def execute
options = parse_options
puts "Options: #{options.inspect}" if options[:debug]
files = get_changed_files(branch_name: BRANCH_NAME)
puts "Files: #{files.inspect}" if options[:debug]
base_files = files.map { |path| File.basename(path) }
puts "Base files: #{base_files.inspect}" if options[:debug]
if base_files.empty?
puts 'No migration files found'
exit 1
end
selected_files = prompt(base_files, prompt: 'Select migrations (press tab to select multiple)> ', multi: true)
puts "Selected files: #{selected_files.inspect}" if options[:debug]
if selected_files.empty?
puts 'No files selected'
exit 1
end
sorted = selected_files.sort_by { |f| f.match(/^\d+/)[0].to_i }
puts "Sorted: #{sorted.inspect}" if options[:debug]
case options[:task].to_sym
when :up
sorted.each do |file|
version = file.match(/^\d+/)[0].to_i
cmd = "bin/rails db:migrate:up:main db:migrate:up:ci VERSION=#{version}"
puts "$ #{cmd}"
raise "Migration #{version} failed" unless system(cmd)
end
when :down
sorted.reverse_each do |file|
version = file.match(/^\d+/)[0].to_i
cmd = "bin/rails db:migrate:down:main db:migrate:down:ci VERSION=#{version}"
puts "$ #{cmd}"
raise "Migration #{version} failed" unless system(cmd)
end
end
end
# rubocop:enable Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
require_commands!('fzf')
execute
|