Created
May 13, 2010 18:25
-
-
Save bryanstearns/400184 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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