Skip to content

Instantly share code, notes, and snippets.

@bryanstearns
Created May 13, 2010 18:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bryanstearns/400184 to your computer and use it in GitHub Desktop.
Save bryanstearns/400184 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
#
# Don't run this - it's a waste of time...
#
# We got ourselves into a situation where we had several undeployed migrations
# that depended on intermediate versions of our models to evolve production
# data, which prevented easy deployment.
#
# I wrote this script, assuming that it could "figure out" how to check out
# intermediate revisions and migrate in several steps. Unfortunately, this
# was a bad assumption: you can't tell what revision is needed to run a
# particular migration -- it might be the latest revision in which the
# migration file changed, or something newer than that, but if you go too
# far, you get to changes that won't run, because not every revision is
# functional in our world (for shame!).
#
# I'm stashing this on github because some parts of it might be useful someday.
require 'rubygems'
require 'optparse'
require 'ruby-debug'
require 'grit'
class Migrator
def initialize
@actions = {}
@verbose = false
@dryrun = nil
@force = false
@upto = nil # --migrate: up to but not this migration
OptionParser.new do |opts|
opts.banner = "Usage: migrator [options]"
opts.on("--dryrun", "Say, don't do.") { |@dryrun| }
opts.on("--upto VERSION", "For --migrate, up to but not this migration") { |upto| @upto = upto.to_i }
opts.on("-v", "--verbose", "Be chatty") { |@verbose| }
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit 2
end
end.parse!
run("git checkout vendor/plugins/haml/init.rb")
@repo = Grit::Repo.new(".")
abort "Working tree isn't clean" \
unless clean_index?
@head = @repo.head.name
@dump_migration_timestamp = get_dump_migration_timestamp
@migration_names = get_migration_names
@timestamps = @migration_names.keys.sort
@timestamps = @timestamps[@timestamps.index(@dump_migration_timestamp)+1..-1]
if @upto
upto_version = @timestamps.index(@upto) || abort("--upto version not found")
@timestamps = @timestamps[0..upto_version-1]
end
abort "Nothing to do" if @timestamps.empty?
puts "will migrate : #{@timestamps.join(", ")}"
# Find the revision of the dump's migration
oldest_revision = \
newest_change_revision("db/migrate/#{@migration_names[@dump_migration_timestamp]}")
# Find the most recent change to each migration we want
revision_map = @timestamps.inject({}) do |h, timestamp|
h[timestamp] = newest_change_revision("db/migrate/#{@migration_names[timestamp]}")
h
end
# Get the commits in order from just after the dump's revision to now,
# ignoring any not associated with migrations
revisions_in_order = get_commits_since(oldest_revision).delete_if do |revision|
skip = !revision_map.has_value?(revision)
puts "Skipping #{revision[0..8]}: not associated with a migration" \
if skip && @verbose
skip
end
# Figure out what index in the revision list each revision changed at
revision_indexes = @timestamps.map do |timestamp|
result = revisions_in_order.index(revision_map[timestamp]) || \
abort("Can't find index for #{timestamp}")
puts "#{@migration_names[timestamp]} last changed at #{revision_map[timestamp][0..8]}/##{result}"
result
end
# Migrate in ascending order with no backtracking
last_index_done = -1
revision_indexes.each do |index|
revision = revisions_in_order[index]
if index <= last_index_done
puts "skipping #{revision[0..8]}/##{index} because we've already done #{last_index_done}" if @verbose
next
end
puts "#{'(not) ' if @dryrun}migrating at #{revision[0..8]}/##{index}"
migrate_at(revision)
last_index_done = index
end
checkout(@head) # put us back where we started
puts "Done" if @verbose
end
def clean_index?
%w[added changed deleted].all? {|set| @repo.status.send(set).empty? }
end
def run(cmd)
output = `#{cmd}`
abort "#{cmd} failed (#{$?})" unless $? == 0
output
end
def get_dump_migration_timestamp
migrations = []
File.open("db.production.sql").each_line do |line|
if line =~ /INSERT INTO `schema_migrations` VALUES \('(\d+)'\);/
migrations << $1.to_i
elsif migrations.any?
break
end
end
result = migrations.max
puts "dump is at #{result}" if @verbose
result
end
def get_migration_names
Dir.glob("db/migrate/*.rb").inject({}) do |h, filename|
filename = File.basename(filename)
h[filename.to_i] = filename
h
end
end
def newest_change_revision(path)
result = run("git log -1 #{path}").split('\n')[0].split(' ')[1]
puts "#{path} last changed at #{result[0..8]}" if @verbose
result
end
def get_commits_since(revision)
cmd = "git log #{revision}..HEAD | grep ^commit"
#puts "Recent commit cmd: #{cmd.inspect}" if @verbose
result = run(cmd).split("\n").map {|c| c.split(' ')[1] }.reverse
#puts "Recent commits: #{result.inspect}" if @verbose
result
end
def migrate_at(revision)
unless @dryrun
checkout(revision)
run("rake db:migrate")
end
end
def checkout(revision)
unless @dryrun
# Make sure innocuous changes to these files don't stop us from checking out
run("git checkout db/schema.rb vendor/plugins/haml/init.rb")
run("git checkout #{revision}")
end
end
end
begin
Migrator.new
rescue SystemExit
raise
rescue Exception => e
puts "Exception: #{e.class}: #{e.message}\n\t#{e.backtrace.join("\n\t")}"
exit 1
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment