Patching Rails and gems locally 21/10/2009

How many times did you have to wait ages until a patch that you’ve submitted gets merged into the Rails or a gem? Sometimes you can’t wait, it’s confusing to keep external libraries in your repository. Fortunatelly – Rails provides a simple way to unpack dependencies into vendor directory, where it’s possible to make local changes. Instead of tracking whole vendor, you can have small .patch files in a separate directory and apply them to frozen vendor stuff. You won’t need to remember which gems are locally modified, repository stays small, patches can be easily applied on your remote production server by invoking rake tasks (you can use Capistrano as well). All you need is a simple Rake task:

namespace :patch do
  desc "Applies patches to Rails frozen in vendor"
  task :rails do
    rails_dir = Rails.root.join("vendor", "rails")
    Rails.root.join("patch", "rails").children.each do |patch|
      patch(patch, rails_dir)
    end
  end

  desc "Applies patches to gems frozen in vendor"
  task :gems do
    Rails.root.join("patch", "gems").children.each do |patch_dir|
      patch_dir.children.each do |patch|
        gem_dir = Rails.root.join("vendor", "gems", patch_dir.basename)
        patch(patch, gem_dir)
      end
    end
  end

  desc "Applies patches to Rails and gems in vendor"
  task :all => ["gems", "rails"]
end

def patch(patch, dir)
  puts "Applying patch #{patch.basename} to #{dir.basename}"
  sh "patch -N -p 1 -d #{dir} -i #{patch}"
end

Freeze your Rails and/or gems using rake rails:freeze:gems and rake gems:unpack. You have to store your patches as shown below. Notice that gem’s patches directories must include the version number (just like in vendor/gems).

To apply the patches run rake patch:gems or rake patch:rails task (rake patch:all). Don’t forget to add vendor/rails and vendor/gems to your .gitignore.