Skip to content

Instantly share code, notes, and snippets.

@chabgood
Created January 2, 2019 18:50
Show Gist options
  • Save chabgood/7b24c5b5463c51f1683319fdcd077308 to your computer and use it in GitHub Desktop.
Save chabgood/7b24c5b5463c51f1683319fdcd077308 to your computer and use it in GitHub Desktop.
save state db when changing branch
#!/usr/bin/env ruby
require 'yaml'
class UnsupportedDatabaseAdapter < RuntimeError
attr_reader :adapter
def initialize(adapter)
@adapter = adapter
end
def message
"Adapter `#{adapter}` is not supported."
end
end
# If this was a branch checkout
if ARGV[2] == '1'
def dump(env, branch_name)
print "Saving state of #{env} database on '#{branch_name}' branch..."
if system(dump_cmd(env, branch_name))
print "done!\n"
true
else
print "failed!\n"
false
end
rescue UnsupportedDatabaseAdapter => e
puts "\nERROR: #{e.message}"
end
def dump_file(database_name, branch_name)
# Replace any special characters that may cause file system issues
branch_name = branch_name.gsub(/[^0-9a-z.\-_]/i, '_')
"#{@dump_folder}/#{database_name}-#{branch_name}"
end
def dump_cmd(env, branch_name)
adapter = @rails_db_config[env]['adapter']
case adapter
when 'postgresql'
pg_dump_cmd(env, branch_name)
when 'mysql2'
mysql_dump_cmd(env, branch_name)
else
raise UnsupportedDatabaseAdapter.new(adapter)
end
end
def pg_dump_cmd(env, branch_name)
database_name = @rails_db_config[env]['database']
%[pg_dump --file="#{dump_file(database_name, branch_name)}" #{database_name}]
end
def mysql_dump_cmd(env, branch_name)
database_name = @rails_db_config[env]['database']
username = @rails_db_config[env]['username']
password = @rails_db_config[env]['password']
%[mysqldump --add-drop-database --user=#{username} --password="#{password}" --databases #{database_name} > "#{dump_file(database_name, branch_name)}"]
end
def restore(env, path)
system(restore_cmd(env, path))
end
def restore_cmd(env, path)
adapter = @rails_db_config[env]['adapter']
case adapter
when 'postgresql'
pg_restore_cmd(env, path)
when 'mysql2'
mysql_restore_cmd(env, path)
else
raise UnsupportedDatabaseAdapter.new(adapter)
end
end
def pg_restore_cmd(env, path)
database_name = @rails_db_config[env]['database']
drop_existing_pg_connections_to_database(database_name)
drop_pg_database(database_name)
create_pg_database(database_name)
%[psql --file="#{path}" #{database_name} > /dev/null]
end
def drop_existing_pg_connections_to_database(database_name)
system(%[psql --command="SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '#{database_name}' AND pid <> pg_backend_pid();" postgres > /dev/null])
end
def drop_pg_database(database_name)
system(%[dropdb #{database_name}])
end
def create_pg_database(database_name)
system(%[createdb #{database_name}])
end
def mysql_restore_cmd(env, path)
database_name = @rails_db_config[env]['database']
username = @rails_db_config[env]['username']
password = @rails_db_config[env]['password']
%[mysql -u #{username} --password="#{password}" #{database_name} < "#{path}"]
end
def branches_from_refhead(ref)
`git show-ref --heads | grep #{ref} | awk '{print $2}'`.split("\n").map{ |b| b.sub(/^refs\/heads\//, '') }
end
# Get the current (destination) branch
@destination_branch = `git rev-parse --abbrev-ref HEAD`.strip
# Since we're just given a commit ID referencing the branch head we're coming from,
# it could be at the head of multiple branches. We can assume the source isn't the same as the
# destination branch, so we can remove that immediately.
@source_branches = branches_from_refhead(ARGV[0]).reject{ |b| b == @destination_branch }
@project_root = %x[git rev-parse --show-toplevel].strip
@dump_folder = "#{@project_root}/.db_branch_dumps"
# Load Rails DB config and grab database name
@rails_db_config = YAML.load_file("#{@project_root}/config/database.yml")
dev_database_name = @rails_db_config['development']['database']
# Ensure dump directory exists
unless Dir.exists?(@dump_folder)
Dir.mkdir @dump_folder
end
# Don't do anything if the source and destination branches are the same or nonexistent
unless @source_branches.include?(@destination_branch) || @source_branches.empty? || (@source_branches | [@destination_branch]).any?{ |b| b == '' }
# Dump database for source branches
if @source_branches.all? { |branch| dump('development', branch) }
# Restore dump from this branch, if it exists
dump_path = dump_file(dev_database_name, @destination_branch)
if File.exists?(dump_path)
print "Restoring #{dev_database_name} to its previous state on this branch..."
if restore('development', dump_path)
print "done!\n"
# If we have a structure.sql file, restore that to the test database,
# otherwise fall back to rake
structure_sql = File.join(@project_root, 'db', 'structure.sql')
if File.exists?(structure_sql)
print "Restoring test database from structure.sql..."
print restore('test', structure_sql) ? "done!\n" : "failed!\n"
elsif File.exists?("#{@project_root}/Rakefile")
print "Preparing test database..."
rake_cmd = "rake db:test:prepare"
if File.exists?("#{@project_root}/bin/rake")
rake_cmd = "./bin/#{rake_cmd}"
elsif File.exists?("#{@project_root}/Gemfile")
rake_cmd = "bundle exec #{rake_cmd}"
end
system rake_cmd
print "done!\n"
else
print "No structure.sql or Rakefile detected, skipping test database restoration\n"
end
else
print "failed!\n"
end
else
print "No DB dump for #{dev_database_name} on the '#{@destination_branch}' branch was found!\n"
print "The state of your database has been saved for when you return to the '#{@source_branches.join('\' or \'')}' branch, but its current state has been left unchanged. You are now free to make changes to it that are specific to this branch, and they will be saved when you checkout a different branch, then restored when you checkout this one again.\n"
end
else
print "Failed to dump database. Halting.\n"
end
end
end
@chabgood
Copy link
Author

chabgood commented Jan 2, 2019

chmod ug+x .git/hooks/*
put it in the post-checkout git hook.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment