There are lots of “text compare tools” online, some of them are even monetized.

diff-rephrase

For me as a developer, github is the most familiar tool for viewing the difference of a file before and after changes.

diff-github-split-unufied

Sometimes in a Rails app we want to track changes to attributes on a database table (how a post.title or post.body changed). Let’s build this feature:

audited-diff-column-split-views

Google Docs have a great implementation for change history:

version control in google docs

Here’s how you can add version control and display changes in your app!

Store changes #

First, let’s create a db table:

rails g scaffold post title:string body:text

Log all changes to a rails model with gem audited

bundle add audited
rails generate audited:install
rails db:migrate

Audit all changes on all attributes. You can also limit what is audited (check the official gem docs).

# app/models/post.rb
class Post < ApplicationRecord
  audited
end

Display change (audit) history:

# app/views/posts/show.html.erb
Change count: <%= @post.audits.count %>

<% @post.audits.sort_by(&:version).reverse.each do |audit| %>
  <div style="border: 1px solid black; padding: 10px">

    <%= audit.action %>
    <%= audit.version %>
    <%= audit.created_at %>

    <% audit.audited_changes.each do |attribute, changes| %>
      <div>
        <b><%= attribute %></b>
        <% if changes.is_a? Array %>
          <%= changes[0], changes[1] %>
        <% elsif changes.is_a? String %>
          <%= changes %>
        <% end %>
      </div>
    <% end %>

  </div>
<% end %>

Display changes #

Diffy is a library that creates diffs between strings in an HTML-friendly way. It’s all you need to turn the difference between two strings into something that looks like GIT!

bundle add diffy

Update your views to use Diffy for the changes:

# app/views/posts/show.html.erb
Change count: <%= @post.audits.count %>

<% if params[:split] == "true" %>
  <%= link_to 'Column view', post_path(@post) %>
<% else %>
  <%= link_to 'Split view', post_path(@post, split: true) %>
<% end %>

<% @post.audits.sort_by(&:version).reverse.each do |audit| %>
  <div style="border: 1px solid black; padding: 10px">

    <%= audit.action %>
    <%= audit.version %>
    <%= audit.created_at %>

    <% audit.audited_changes.each do |attribute, changes| %>
      <div>
        <b><%= attribute %></b>
        <% if changes.is_a? Array %>
          <% if params[:split] %>
            <div style="display: flex">
              <%= Diffy::SplitDiff.new(changes[0], changes[1], format: :html).left.html_safe %>
              <%= Diffy::SplitDiff.new(changes[0], changes[1], format: :html).right.html_safe %>
            </div>
          <% else %>
            <%= Diffy::Diff.new(changes[0], changes[1], include_plus_and_minus_in_html: true, include_diff_info: true).to_s(:html).html_safe %>
          <% end %>
        <% elsif changes.is_a? String %>
          <%= Diffy::Diff.new("", changes, include_plus_and_minus_in_html: true).to_s(:html).html_safe %>
        <% end %>
      </div>
    <% end %>

  </div>
<% end %>

Finally, run Diffy::CSS to generate CSS that you can paste into your application.css:

/* app/assets/stylesheets/application.css */
.diff{overflow:auto;}
.diff ul{background:#fff;overflow:auto;font-size:13px;list-style:none;margin:0;padding:0;display:table;width:100%;}
.diff del, .diff ins{display:block;text-decoration:none;}
.diff li{padding:0; display:table-row;margin: 0;height:1em;}
.diff li.ins{background:#dfd; color:#080}
.diff li.del{background:#fee; color:#b00}
.diff li:hover{background:#ffc}
/* try 'whitespace:pre;' if you don't want lines to wrap */
.diff del, .diff ins, .diff span{white-space:pre-wrap;font-family:courier;}
.diff del strong{font-weight:normal;background:#fcc;}
.diff ins strong{font-weight:normal;background:#9f9;}
.diff li.diff-comment { display: none; }
.diff li.diff-block-info { background: none repeat scroll 0 0 gray; }

Final result - normal view:

audited-diff-column

Final result - split view:

audited-diff-split

Code example on github

That’s it! Now you can log all your changes and display change history in a readable way! 🤠