The goal of Revisionary is to bring over as much as I can from the world of SCM to ActiveRecord. There are a number of versioning plugins, but they're generally limited to the scope of an individual table (i.e. single model) and usually fall short when handling associations.
acts_as_revisable was the premise of this plugin, and a lot of the concepts used in Revisionary are adopted from that project. I wanted to be able to drop the need for a second class, and also wanted to carry over a lot of things I love from git, such commit hashes as opposed to numeric versions. Most importantly, I needed to ability to atomically commit a model and any associations I want.
acts_as_revisable brought branching and basic association cloning to ActiveRecord. What if you could create a record, some associations, and be able to commit that record and its associations without worrying about much? A page has many parts, if I modify attributes on the page and/or the parts, I want to be able to 'commit' the page and take a snapshot of the current attributes for all affiliated models. With Revisionary, this is now possible, and by intelligently hashing the content of both the parent record and associated records, saving (committing) only takes place if content changes.
How it Works
Commits are saved with a SHA1 hash generated by concatenating the values of the parent model and the values of it's associations. Each snapshot consists of a record containing the parent object, and n-records for it's associations.
- Page: Home Page - Part: Welcome to our site! ... part data changed and stored ... - Page: Home Page - Part: Hello from our site! ... page data changed and stored ... - Page: Our Home Page - Part: Hello from our site!
class Page < ActiveRecord::Base has_many :parts is_revisionary :with => :parts end class Part < ActiveRecord::Base belongs_to :page end
In the Page definition, the `with` option accepts either an array or a single association. Now, any modifications to the parts association will be monitored by page records.
@page = Page.create :name => "Home Page" @part = @page.parts.create :name => "Body", :content => "Welcome!" @page.head? # => true
Modifications made to an association will be committed atomically with the parent object if saved.
@part.content = "Welcome to our website!" @page.save @page.ancestors.size # => 1
A snapshot of that commit has been stored to the database, and updates made to either the parent or associations will be stored, again, atomically. No need to save your associations.
Revisionary will only save a snapshot if data has been altered.
@page.ancestors.size # => 1 @page.save @page.ancestors.size # => 1 @page.name = "About Us" @page.save @page.ancestors.size # => 2
It is sometimes helpful to commit with a message
@page.save :commit_message => "Made some edits"
Not feelin' SHA1 hashes? Tag your commits.
@page.save :tag => "final-2"
Not every save needs to be commited.
@page.save :without_commit => true
Checkouts / Reverting
Checking out older commits is extremely easy to do.
@page.checkout(:previous) # Previous commit @page.checkout(:root) # Base commit for branch @page.checkout(6) # 6 commits ago @page.checkout("tag:final-2") # Revision tagged 'final-2' @page.checkout("f62e0ea") # Like in git, you only need enough of a commit hash to guarantee uniqueness
This will return, not revert to, the requested commit.
To add a previous version to the head, you can quickly revert to any commit:
Branching has been removed for now. I don't see many practical uses in the project that this is being developed for, but perhaps this can be reintroduced at a later time?
Diffing / Merging
No extra tables, just a few columns to one table
Revisionary will only work under Rails Edge, or anything tagged >= 2.1
Many thanks go to Rich Cavanaugh, the author of acts_as_revisable. Not only was his project my inspiration in creating Revisionary, but his acts_as_scoped_model and other code samples propelled me to finish most of this plugin while on a plane.
Copyright © 2008 Brennan Dunn, released under the MIT license