25 Mar 2009

Using Git Submodules for Shared Application Components

In some cases you may have the need to run multiple Rails applications with shared functionality. While Rails 3 promises to bring “mounting apps in apps” and the ability to make the whole process simple, for now we’re stuck in the real world. However, it is possible to share components. This post will walk you through how to set up shared components that live in multiple Rails applications at once and even run specs properly.

A few notes before we begin:

  • This post focuses on models but the same could be applied to controllers, views, and more.
  • I use RSpec so the testing solutions come from that perspective.
  • My applications share a database, so I keep all of the migrations in one app and load from a duplicated schema.rb in the other.

Setting Up Your Application

First, you’ll need to create your shared directory. I’m mounting all shared components to my RAILS_ROOT/shared directory. So if I have app1 and app2 then I’ll do this in app1:

mkdir shared
cd shared
touch README
git init
git add .
git commit -m "Initial import of shared repository."
git remote add origin git@some.git:repository/online.git
git push

At this point all we’ve actually done is create a git repository inside our app1 application in the shared directory and pushed it (with an empty README file) to a remote git repository.

What we need to do now is actually delete this git repository and re-add it as a submodule from the remote source (this is from the root of app1 again):

git submodule add git@some.git:repository/online.git shared
git submodule init
git add .
git commit -m "Added shared directory as submodule"

What we did here is add the repository as a submodule using the git submodule command. We then ran git submodule init to update our .git/config to reflect the new submodule.
Finally we committed our changes.

So now we have a submodule living in our application directory, but right now it’s empty and Rails doesn’t know or care about it! Next we’ll set up Rails to make use of the components in the shared directory.

Setting Up Rails To Use Shared Components

Lets say that we’re going to create a shared model, SharedModel. We need to put it in the shared directory but still have it picked up by Rails’s lazy class loader. So in config/environment.rb you
will need to add the following:

config.load_paths += %W( #{RAILS_ROOT}/shared/models )

This tells Rails to look for classes in the shared models path. Now we create our model by creating shared/models/shared_model.rb:

class SharedModel < ActiveRecord::Base

end

When creating shared components I tend not to use Rails’s model generator, preferring instead to create the class by hand and generate a migration separately in my migration-having app.

This is actually all you need to do to get your shared components running in Rails. Next we’ll set up app2 to use the same code!

Setting Up The Second Application

To set up the second application, you basically need to simply repeat the same steps you did for the first application starting with git submodule add. So that would be:

  • Add the submodule and initialize it.
  • Add the shared directory to the load paths in config/environment.rb

As a note, if you are doing this to an existing application with multiple developers, other developers will simply need to pull from your main application once you’ve pushed it to a remote and run:

git submodule init
git submodule update

To get the latest revision of the submodule locally for themselves.

Changing Shared Components

To modify shared components, just change them like you would normal files in your repository. The only difference is that when you want to commit changes you will need to do so from the shared directory, push and then make a new commit from the root directory. This way you are telling the root repository to track a new revision of the
submodule.

Testing Shared Components

So just because we’re sharing components doesn’t mean that we want to abandon TDD, does it? In fact, it brings up a somewhat interesting problem. I want to have specs that I can run for the shared components that can run in both applications, in fact I want these specs to run in both applications to make sure that the shared components aren’t
having any compatibility issues. While this isn’t extremely difficult to set up, it’s not easy, either.

The first step is to create a spec directory inside your shared submodule, and create a spec_helper.rb that simply points back out to the root application’s.

In shared/spec/spec_helper.rb:

require File.dirname(__FILE__) + '/../../spec/spec_helper'

We also need to create a pending spec for our SharedModel to make sure that these are running. In shared/spec/models/shared_model_spec.rb:

require File.dirname(__FILE__) + '/../spec_helper'

describe SharedModel do
  it 'should have a pending spec'
end

The good news is that if you run autospec from your shared directory, you should be able to see your pending spec run (you will need to create a second spec.opts file in the shared/spec directory for this to use your preferred options). You should push out the changes in your shared directory and get all of your applications up to date. The bad news is that this is the only place your specs will run at the moment. Let’s change that for the better.

Note: You will need to perform the following steps in each of your Rails apps that use the shared components.

First to get your specs running with rake spec we will need to modify the task found in lib/tasks/rspec.rake by performing the following substitution:

# When you see this...
FileList['spec/**/*/*_spec.rb']

# Change it to this...
FileList['spec/**/*/*_spec.rb', 'shared/spec/**/*/*_spec.rb']

That takes care of the spec rake tasks, but there’s still the matter of autotesting from your application root. This requires a custom .autotest file in your application root that looks like this:

Autotest.add_hook :initialize do |autotest|
  autotest.add_mapping(%r%^shared/models/.*\.rb$%) { |_, m|
    autotest.files_matching %r%^shared/spec/.*_spec.rb$%
  }
end

This will automatically pick up changes to your shared models and re-run the specs for all of your shared models when they change. You could get more granular than this, but that’s a topic for another day.

Wrapping Up

Now that all of this is done you should be able to freely develop models in the shared directory and have them completely integerated into your normal development workflow. It’s quite a bit of work, but once you iron out the kinks it runs beautifully!

blog comments powered by Disqus