Rails vs SCM: resolving conflicts between local and upstream Migrations

If you’re working on a local branch of a Rais project for long enough, you’re bound to run into this irritating problem: you create a new migration, it gets the smallest unique number from the ones you got from upstream, BUT, before you get the chance to commit it, someone does it first, and in your next update (svn up || git pull) you have that tangled migration mess.

This little rake task might help you out. Warning: it assumes that all your local migrations have already been run, and that *none* of the new migrations from upstream have been run.

The code is definetely not very DRY and doesn’t take much advantage of Rake (I’m pretty n00b on Rake), so I accept suggestions/patches :)

To use it, just throw it in your lib/tasks folder and call it using “rake db:migrate:fast_forward”.

Next (and easy) step is making it receive a SCM parameter (git/svn) so it’ll use the proper “mv” command.


namespace :db do
  namespace :migrate do
    desc <<STR
Resolves conflicts between local and upstream migrations.

This task assumes the following scenario:
During your local development, you've created migrations and ran rake db:migrate;
Then, you updated from upstream (svn update || git svn rebase), and ended up with
pairs of migrations with the same number: one is the local you created, and the
other is the one from upstream that someone commited before you.

Besides that, there might be some other non-overlapping migrations *after* the
overlapping zone that are *also* local (you had more local migrations than new ones
that came from upstream on the update).

This tasks takes *all your local migrations*, **reverts them** (in reverse order),
and moves them (in order) to the end of the line.

After that you can run rake db:migrate again and it'll run first the migrations
from upstream, and yours last.
STR

    task :fast_forward => :environment do
      migrator = ActiveRecord::Migrator.new(:down, 'db/migrate')
      puts "Looking for migrations with repeated numbers"
      all_migrations = Dir['db/migrate/*'].sort
      pairs = all_migrations.group_by{|migration| migration =~ /(\d+)/; $1}.
        select {|number, migrations| 1 < migrations.size && migrations.size < 3}
      pairs = pairs.sort {|x, y| x[0] <=> y[0]}

      # Pick the range of (local) migrations that will be slided to the end
      migrations_to_move = []
      # First the ones that overlap (disambiguated by user)
      pairs.map{|pair| pair[1]}.each do |mig1, mig2|
        begin
          puts "\n[1]\t#{mig1}"
          puts "[2]\t#{mig2}"
          puts "\nWhich one is part of the range to be slided to the end of the list?"
          option = STDIN.gets.to_i
        end until option == 1 || option == 2

        migrations_to_move << (option == 1 ? mig1 : mig2)

      end
      # Then the (local) ones past the overlap zone
      unless pairs.empty?
        idx_last_overlapping_migration = all_migrations.index(pairs.last[1].last)
        migrations_to_move += all_migrations[idx_last_overlapping_migration+1..-1]
        # Assumes all (and only) the local ones past the overlap zone have already been run
        migrations_to_move.reject! { |m| m =~ /(\d+)/; $1.to_i > migrator.current_version }
      end

      migrations_to_move.first =~ /(\d+)/
      schema_version = $1.to_i # set_schema_version subtracts one

      # Slide the range to be slided to the end of the list
      upstream_migrations = all_migrations - migrations_to_move
      upstream_migrations.last =~ /(\d+)/
      next_number = $1.to_i + 1

      new_names = migrations_to_move.map { |migration|
        migration =~ /(\d+)(.*)/
        name_migration_to_move = $2

        new_name = 'db/migrate/' + ("%03d" % next_number) + name_migration_to_move
        next_number += 1
        new_name
      }

      # Confirm and execute
      unless migrations_to_move.empty?
        pp "Latest upstream migrations", upstream_migrations.last(5)
        pp "These are your local migrations: ", migrations_to_move
        pp "They will be reverted and renamed to: ", new_names
        puts "And the new schema version will be: #{schema_version-1}"

        begin
          puts "\nShould I proceed? [Y/n] "
          option = STDIN.gets.strip.downcase
        end until option == 'y' || option == 'n'

        if option == 'y'
          # Revert
          migrations_to_move.reverse.each do |migration|
            require migration
            migration_class = migrator.send(:migration_class, *(migrator.send(:migration_version_and_name, migration).reverse))
            migration_class.down
          end
          migrator.send(:set_schema_version, schema_version)
          # Move to end of line
          migrations_to_move.zip(new_names) do |old_name, new_name|
            File.rename old_name, new_name
          end
        end
      else
        puts "No overlapping migrations. You can safely run rake db:migrate."
      end
    end
  end
end

Update: Just after writing this, a friend told me about the Git Migration Buddy. It is git specific and seems to handle handle multiple branches better. Mine is kinda 1-n (main (svn in my case) repo syncing with multiple local branches). There’s the enhanced_migrations plugin that supposedly stops the problem at the root, having timestamps instead of increasing numbers for migrations. Zach in the comments also mentions a great solution he’s coming up with: a post-checkout hook to change database.yml and have a different db for each branch (dunno if it works too well with big dbs, but it’s a great idea nonetheless).

Leave a Reply