File: migrate.rb

package info (click to toggle)
gitlab 17.6.5-19
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 629,368 kB
  • sloc: ruby: 1,915,304; javascript: 557,307; sql: 60,639; xml: 6,509; sh: 4,567; makefile: 1,239; python: 406
file content (135 lines) | stat: -rwxr-xr-x 4,135 bytes parent folder | download
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