├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── brancher.gemspec ├── lib ├── brancher.rb └── brancher │ ├── auto_copying.rb │ ├── database_configuration_renaming.rb │ ├── database_rename_service.rb │ ├── multiple_database_configuration_renaming.rb │ ├── railtie.rb │ └── version.rb └── spec ├── brancher └── database_rename_service_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | 16 | .rspec 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.0 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in brancher.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Naoto Kaneko 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brancher 2 | 3 | [![Build Status](https://travis-ci.org/naoty/brancher.svg)](https://travis-ci.org/naoty/brancher) 4 | [![Code Climate](https://codeclimate.com/github/naoty/brancher/badges/gpa.svg)](https://codeclimate.com/github/naoty/brancher) 5 | 6 | Brancher is a rubygem to switch databases connected with ActiveRecord by Git branch. 7 | 8 | For example, if the name of a database is `sample_app_dev`, Brancher will switch the database to `sample_app_dev_master` at `master` branch, and `sample_app_dev_some_feature` at `some_feature` branch. 9 | 10 | ## Installation 11 | 12 | Add this line to your application's Gemfile: 13 | 14 | ```ruby 15 | group :development do 16 | gem "brancher" 17 | end 18 | ``` 19 | 20 | And then execute: 21 | 22 | ``` 23 | $ bundle 24 | ``` 25 | 26 | ## Configuration 27 | 28 | ```ruby 29 | # config/environments/development.rb 30 | 31 | Brancher.configure do |c| 32 | # if branch is "master" or "develop", database name has no suffix. 33 | c.except_branches << "master" 34 | c.except_branches << "develop" 35 | 36 | # if auto_copy is true and database does not exist, 37 | # copy database from no suffix name database to suffixed name one. 38 | c.auto_copy = true 39 | end 40 | ``` 41 | 42 | ## Contributing 43 | 44 | 1. Fork it ( https://github.com/[my-github-username]/brancher/fork ) 45 | 2. Create your feature branch (`git checkout -b my-new-feature`) 46 | 3. Commit your changes (`git commit -am 'Add some feature'`) 47 | 4. Push to the branch (`git push origin my-new-feature`) 48 | 5. Create a new Pull Request 49 | 50 | ## Author 51 | 52 | [naoty](https://github.com/naoty) 53 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | 8 | -------------------------------------------------------------------------------- /brancher.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'brancher/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "brancher" 8 | spec.version = Brancher::VERSION 9 | spec.authors = ["Naoto Kaneko"] 10 | spec.email = ["naoty.k@gmail.com"] 11 | spec.summary = %q{Switching databases connected with ActiveRecord by Git branch} 12 | spec.homepage = "https://github.com/naoty/brancher" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0") 16 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 17 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency "activerecord" 21 | spec.add_dependency "railties" 22 | 23 | spec.add_development_dependency "bundler", "~> 1.7" 24 | spec.add_development_dependency "rake", "~> 10.0" 25 | spec.add_development_dependency "rspec" 26 | spec.add_development_dependency "pry" 27 | end 28 | -------------------------------------------------------------------------------- /lib/brancher.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(__dir__) unless $LOAD_PATH.include?(__dir__) 2 | 3 | require 'active_support/configurable' 4 | 5 | module Brancher 6 | include ActiveSupport::Configurable 7 | config.except_branches ||= [] 8 | config.auto_copy ||= false 9 | config.max_database_name_length ||= 63 10 | end 11 | 12 | require "brancher/database_configuration_renaming" 13 | require "brancher/multiple_database_configuration_renaming" 14 | require "brancher/database_rename_service" 15 | require "brancher/auto_copying" 16 | require "brancher/railtie" 17 | require "brancher/version" 18 | -------------------------------------------------------------------------------- /lib/brancher/auto_copying.rb: -------------------------------------------------------------------------------- 1 | module Brancher 2 | module AutoCopying 3 | def new_connection 4 | done = false 5 | 6 | begin 7 | super 8 | rescue 9 | raise if done 10 | raise unless Brancher.config.auto_copy 11 | 12 | executor = Executor.new(spec.config) 13 | executor.auto_copy 14 | done = true 15 | retry 16 | end 17 | end 18 | 19 | class Executor 20 | attr_reader :config 21 | 22 | def initialize(config) 23 | @config = config 24 | end 25 | 26 | def auto_copy 27 | return if config[:database] == config[:original_database] 28 | 29 | database_name = config[:database] 30 | original_database_name = config[:original_database] 31 | 32 | case config[:adapter] 33 | when /mysql/ 34 | mysql_copy(original_database_name, database_name) 35 | when /postgresql/ 36 | pg_copy(original_database_name, database_name) 37 | end 38 | end 39 | 40 | def mysql_copy(original_database_name, database_name) 41 | ActiveRecord::Tasks::DatabaseTasks.create(config.with_indifferent_access) 42 | 43 | cmd = ["mysqldump", "-u", config[:username]] 44 | cmd.concat(["-h", config[:host]]) if config[:host].present? 45 | cmd.concat(["-p#{config[:password]}"]) if config[:password].present? 46 | cmd << original_database_name 47 | cmd.concat(["|", "mysql", "-u", config[:username]]) 48 | cmd.concat(["-h", config[:host]]) if config[:host].present? 49 | cmd.concat(["-p#{config[:password]}"]) if config[:password].present? 50 | cmd << database_name 51 | system(cmd.join(" ")) 52 | end 53 | 54 | def pg_copy(original_database_name, database_name) 55 | env = {} 56 | env["PGUSER"] = config[:username] if config[:username].present? 57 | env["PGPASSWORD"] = config[:password] if config[:password].present? 58 | env["PGHOST"] = config[:host] if config[:host].present? 59 | 60 | system(env, "createdb", "-T", original_database_name, database_name) 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/brancher/database_configuration_renaming.rb: -------------------------------------------------------------------------------- 1 | module Brancher 2 | module DatabaseConfigurationRenaming 3 | def database_configuration 4 | configurations = super 5 | DatabaseRenameService.rename!(configurations) 6 | configurations 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/brancher/database_rename_service.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | 3 | module Brancher 4 | module DatabaseRenameService 5 | extend self 6 | 7 | def rename!(configurations, key = env) 8 | configuration = configurations[key] 9 | configuration["original_database"] = configuration["database"] 10 | configuration["database"] = database_name_with_suffix(configuration["database"]) 11 | configurations 12 | end 13 | 14 | private 15 | 16 | def database_name_with_suffix(database) 17 | database_extname = File.extname(database) 18 | database_name = database.gsub(%r{#{database_extname}$}) { "" } 19 | database_name += suffix unless database_name =~ %r{#{suffix}$} 20 | database_name += database_extname 21 | database_name = database_name.slice(0,Brancher.config.max_database_name_length-22) + [Digest::MD5.digest(database_name)].pack("m0").slice(0,22).gsub(/[^\w]/, '_').downcase if database_name.length > Brancher.config.max_database_name_length 22 | database_name 23 | end 24 | 25 | def suffix 26 | return nil if current_branch.blank? || Brancher.config.except_branches.include?(current_branch) 27 | "_#{current_branch}" 28 | end 29 | 30 | def env 31 | Rails.env 32 | end 33 | 34 | def current_branch 35 | @current_branch ||= `git rev-parse --abbrev-ref HEAD`.chomp 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/brancher/multiple_database_configuration_renaming.rb: -------------------------------------------------------------------------------- 1 | module Brancher 2 | module MultipleDatabaseConfigurationRenaming 3 | module ClassMethods 4 | def establish_connection(spec = nil) 5 | DatabaseRenameService.rename!(configurations, spec.to_s) if spec && spec.is_a?(Hash).! 6 | 7 | super 8 | end 9 | end 10 | 11 | def self.prepended(base) 12 | base.singleton_class.send(:prepend, ClassMethods) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/brancher/railtie.rb: -------------------------------------------------------------------------------- 1 | require "rails" 2 | 3 | module Brancher 4 | class Railtie < Rails::Railtie 5 | initializer "brancher.rename_database", before: "active_record.initialize_database" do 6 | Rails::Application::Configuration.send(:prepend, DatabaseConfigurationRenaming) 7 | ActiveRecord::Base.send(:prepend, MultipleDatabaseConfigurationRenaming) 8 | ActiveRecord::ConnectionAdapters::ConnectionPool.send(:prepend, AutoCopying) 9 | end 10 | 11 | rake_tasks do 12 | namespace :db do 13 | task :load_config do 14 | require_environment! 15 | DatabaseRenameService.rename!(ActiveRecord::Base.configurations) 16 | end 17 | end 18 | end 19 | 20 | def require_environment! 21 | return unless defined? Rails 22 | environemnt = "#{Rails.root}/config/environments/#{Rails.env}.rb" 23 | require environemnt if File.exists?(environemnt) 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/brancher/version.rb: -------------------------------------------------------------------------------- 1 | module Brancher 2 | VERSION = "0.3.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/brancher/database_rename_service_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Brancher::DatabaseRenameService do 4 | describe ".rename" do 5 | before do 6 | allow(Brancher::DatabaseRenameService).to receive_messages( 7 | current_branch: branch, 8 | env: env 9 | ) 10 | end 11 | 12 | let(:configurations) do 13 | { 14 | env => { 15 | "adapter" => adapter, 16 | "pool" => 5, 17 | "timeout" => 5000, 18 | "database" => database_name 19 | } 20 | } 21 | end 22 | 23 | let(:adapter) do 24 | "sqlite3" 25 | end 26 | 27 | let(:database_name) do 28 | "db/sample_app_development.sqlite3" 29 | end 30 | 31 | let(:env) do 32 | "development" 33 | end 34 | 35 | let(:branch) do 36 | "master" 37 | end 38 | 39 | let(:new_database_name) do 40 | "db/sample_app_development_#{branch}.sqlite3" 41 | end 42 | 43 | let(:new_configurations) do 44 | { 45 | env => { 46 | "adapter" => adapter, 47 | "pool" => 5, 48 | "timeout" => 5000, 49 | "database" => new_database_name, 50 | "original_database" => database_name 51 | } 52 | } 53 | end 54 | 55 | subject do 56 | Brancher::DatabaseRenameService.rename!(configurations) 57 | end 58 | 59 | it { is_expected.to eq new_configurations } 60 | 61 | context "when the adapter is mysql2" do 62 | let(:adapter) do 63 | "mysql2" 64 | end 65 | 66 | let(:database_name) do 67 | "sample_app_development" 68 | end 69 | 70 | let(:new_database_name) do 71 | "#{database_name}_#{branch}" 72 | end 73 | 74 | it { is_expected.to eq new_configurations } 75 | end 76 | 77 | context "when a database has already renamed" do 78 | let(:database_name) do 79 | "db/sample_app_development_#{branch}.sqlite3" 80 | end 81 | 82 | it { is_expected.to eq new_configurations } 83 | end 84 | 85 | context "when the database name is longer than max_database_name_length" do 86 | let(:adapter) do 87 | "mysql2" 88 | end 89 | 90 | let(:database_name) do 91 | "this_is_a_very_long_sample_app_development_database_name_that_exceeds_the_default_set_max_database_name_length" 92 | end 93 | 94 | let(:new_database_name) do 95 | "this_is_a_very_long_sample_app_developmen#{[Digest::MD5.digest("#{database_name}_#{branch}")].pack('m0').slice(0,22).gsub(/[^\w]/, '_').downcase}" 96 | end 97 | 98 | it { is_expected.to eq new_configurations } 99 | end 100 | 101 | context "when it connect another database" do 102 | let(:adapter) do 103 | "mysql2" 104 | end 105 | 106 | let(:database_name) do 107 | "sample_app_another_db" 108 | end 109 | 110 | let(:new_database_name) do 111 | "#{database_name}_#{branch}" 112 | end 113 | 114 | let(:env) do 115 | "another_db" 116 | end 117 | 118 | subject do 119 | Brancher::DatabaseRenameService.rename!(configurations, env) 120 | end 121 | 122 | it { is_expected.to eq new_configurations } 123 | end 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'brancher' 3 | --------------------------------------------------------------------------------