rails-schema-merge-driver

Gem Version CI Ruby Style Guide

A git custom merge driver that auto-resolves the most common conflict in Rails schema files — the bumped define(version: N) line — by keeping the higher version. Real content conflicts are left for a human.

Why

db/schema.rb is regenerated on every migration, so the version: argument at the top of the file changes on every concurrent branch. Whenever two branches add migrations and meet at a merge, git flags this line as a conflict — but it isn't a real conflict: the right answer is always the higher version, which already implies the union of both migrations.

This driver delegates to git's built-in three-way merge, then post-processes the resulting conflict region: if the only diverging line is define(version: N), it picks the higher version and writes the file. Anything else is left as a normal conflict with markers in place, exiting non-zero so git knows the file still needs human attention.

Supported formats

  • ActiveRecord::Schema[X.Y].define(version: N) — standard Rails db/schema.rb.
  • DataMigrate::Data.define(version: N)db/data_schema.rb from the data_migrate gem.

Tolerates merge.conflictstyle = diff3 and zdiff3 (three-section markers).

Installation

Add to your Gemfile in the development group:

gem "rails-schema-merge-driver", group: :development, require: false

require: false is important: the gem ships only an executable, so nothing needs to autoload at app boot. Without it, Bundler.require would either fail looking for a hyphenated path or load lib/rails_schema_merge_driver.rb unnecessarily.

Then bundle install. The git-merge-rails-schema executable becomes available under bundle exec. For a system install instead:

gem install rails-schema-merge-driver

Setup (per repository)

The driver needs to be wired in two places: .gitattributes (which files it applies to) and .git/config (the driver definition itself, which is not versioned and must be set up by every contributor).

1. .gitattributes

db/schema.rb       merge=rails-schema
db/data_schema.rb  merge=rails-schema   # only if you use the data_migrate gem

The second line is only needed for projects that depend on the data_migrate gem.

2. .git/config

Each contributor's clone needs the driver registered. Either run these once:

git config --local merge.rails-schema.name 'keep newer Rails schema version'
git config --local merge.rails-schema.driver 'git-merge-rails-schema %O %A %B %L'

…or — recommended — wire it into your bin/setup so every contributor's clone is configured automatically when they bootstrap the project. Example, adapted from the Librario bin/setup (the project this gem was extracted from):

def configure_git_merge_drivers
  system "git config --local merge.rails-schema.name 'keep newer Rails schema version'"
  system "git config --local merge.rails-schema.driver 'git-merge-rails-schema %O %A %B %L'"
end

configure_git_merge_drivers

git config --local is idempotent, so this is safe to re-run.

If you installed the gem via bundler with require: false, you may need to prefix the driver command with bundle exec so the executable is found from inside the project's gem environment:

git config --local merge.rails-schema.driver 'bundle exec git-merge-rails-schema %O %A %B %L'

How it works

  1. The driver shells out to git merge-file --marker-size=N current base other, which performs the standard three-way text merge.
  2. It reads the result and looks for a conflict region whose only diverging line is a .define(version: N) call (handling diff3/zdiff3 base sections).
  3. If found, it replaces the region with whichever side has the higher numeric version (underscores stripped) and writes the file back.
  4. Exits 0 if the file is fully resolved (no remaining <<<<<<< markers), exits 1 otherwise so git surfaces the remaining conflict to the user.

git merge-file exit codes are passed through carefully: a hard failure (nil or negative status) aborts immediately, since silently treating it as "resolved" would drop the other side's changes.

Development

bin/setup            # bundle install
bundle exec rake     # default: test + standard
bundle exec rake test
bundle exec rake standard

To experiment in an IRB session:

bin/console

To install this gem onto your local machine, run bundle exec rake install.

Releasing

Releases are published to RubyGems.org via trusted publishing (OIDC, no API keys) by .github/workflows/release.yml, triggered when a v* tag is pushed. Steps:

  1. Bump RailsSchemaMergeDriver::VERSION in lib/rails_schema_merge_driver/version.rb.
  2. Move the relevant entries from [Unreleased] to a new dated section in CHANGELOG.md, following the Keep a Changelog format.
  3. Commit and push to main; wait for CI to pass.
  4. Tag and push: sh git tag v0.x.0 git push origin v0.x.0

The workflow then:

  • Authenticates to RubyGems.org via OIDC (no secrets required).
  • Runs bundle exec rake release — which builds the gem and pushes it (skips re-tagging because the tag already exists, per bundler/gem_helper.rb).
  • Creates a GitHub Release at the tag with the CHANGELOG section as notes and the .gem file attached as a downloadable asset.

Acknowledgements

  • Originally extracted from the Librario application (a Rails-based library management system) where this driver was developed and battle-tested.
  • Adapted from tpope's gist, which proposed the original railsschema merge-driver idea.
  • See git's gitattributes(5) docs for the full custom merge-driver protocol.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/tmaier/rails-schema-merge-driver.

License

The gem is available as open source under the terms of the MIT License.