From 06dd7ffe3cd075529941c21f95baccb76a9d84e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Thu, 12 Dec 2013 03:41:34 +0100
Subject: [PATCH] better revision history

---
 Gemfile                                       |    3 -
 Gemfile_rails4.lock                           |   11 -
 .../defer/google_diff_match_patch.js          | 1378 -----------------
 .../controllers/history_controller.js         |  105 +-
 .../javascripts/discourse/models/post.js      |   10 +-
 .../discourse/routes/topic_route.js           |    2 +-
 .../templates/modal/history.js.handlebars     |   56 +-
 .../templates/user/stream.js.handlebars       |    7 +-
 .../discourse/views/modal/hide_modal_view.js  |    2 +-
 .../discourse/views/modal/history_view.js     |    8 +-
 .../discourse/views/modal/modal_body_view.js  |    9 +-
 app/assets/stylesheets/desktop/history.scss   |  116 +-
 app/assets/stylesheets/desktop/user.scss      |    4 +
 app/assets/stylesheets/mobile/history.scss    |   20 -
 app/assets/stylesheets/mobile/user.scss       |    4 +
 app/controllers/posts_controller.rb           |  128 +-
 app/controllers/topics_controller.rb          |    9 +-
 app/models/post.rb                            |   48 +-
 app/models/post_alert_observer.rb             |   13 +-
 app/models/post_revision.rb                   |    6 +
 app/models/topic.rb                           |   28 +-
 app/models/topic_revision.rb                  |    6 +
 app/models/user_action.rb                     |    3 +-
 app/serializers/post_revision_serializer.rb   |   71 +
 app/serializers/post_serializer.rb            |    4 -
 app/serializers/user_action_serializer.rb     |    3 +-
 app/serializers/version_serializer.rb         |   17 -
 config/initializers/vestal_versions.rb        |    9 -
 config/locales/client.en.yml                  |   20 +
 config/routes.rb                              |    2 +-
 .../20131209091702_create_post_revisions.rb   |   25 +
 .../20131209091742_create_topic_revisions.rb  |   25 +
 .../20131210234530_rename_version_column.rb   |    9 +
 lib/cooked_post_processor.rb                  |    2 +-
 lib/diff_engine.rb                            |   16 +-
 lib/discourse_diff.rb                         |  265 ++++
 lib/guardian.rb                               |    4 +
 lib/onpdiff.rb                                |  153 ++
 lib/post_destroyer.rb                         |    2 +-
 lib/post_revisor.rb                           |   14 +-
 spec/components/cooked_post_processor_spec.rb |    2 +-
 spec/components/diff_engine_spec.rb           |   58 -
 spec/components/post_revisor_spec.rb          |   44 +-
 spec/controllers/posts_controller_spec.rb     |   28 -
 spec/models/post_spec.rb                      |   54 +-
 spec/models/topic_spec.rb                     |   28 +-
 vendor/assets/javascripts/bootstrap-modal.js  |    2 +-
 47 files changed, 989 insertions(+), 1844 deletions(-)
 delete mode 100644 app/assets/javascripts/defer/google_diff_match_patch.js
 create mode 100644 app/models/post_revision.rb
 create mode 100644 app/models/topic_revision.rb
 create mode 100644 app/serializers/post_revision_serializer.rb
 delete mode 100644 app/serializers/version_serializer.rb
 delete mode 100644 config/initializers/vestal_versions.rb
 create mode 100644 db/migrate/20131209091702_create_post_revisions.rb
 create mode 100644 db/migrate/20131209091742_create_topic_revisions.rb
 create mode 100644 db/migrate/20131210234530_rename_version_column.rb
 create mode 100644 lib/discourse_diff.rb
 create mode 100644 lib/onpdiff.rb
 delete mode 100644 spec/components/diff_engine_spec.rb

diff --git a/Gemfile b/Gemfile
index 76a25198cd8..b00e9f0b0e1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -69,8 +69,6 @@ gem 'ember-source', '~> 1.2.0.1'
 gem 'handlebars-source', '~> 1.1.2'
 gem 'barber'
 
-gem 'vestal_versions', git: 'https://github.com/SamSaffron/vestal_versions'
-
 gem 'message_bus'
 gem 'rails_multisite', path: 'vendor/gems/rails_multisite'
 gem 'simple_handlebars_rails', path: 'vendor/gems/simple_handlebars_rails'
@@ -124,7 +122,6 @@ gem 'slim'  # required for sidekiq-web
 # URGENT fix needed see: https://github.com/cowboyd/therubyracer/pull/280
 gem 'therubyracer', require: 'v8', git: 'https://github.com/SamSaffron/therubyracer.git'
 gem 'thin', require: false
-gem 'diffy', '>= 3.0', require: false
 gem 'highline', require: false
 gem 'rack-protection' # security
 
diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index 9a630f1e7af..c870737e6f2 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -31,14 +31,6 @@ GIT
       libv8 (~> 3.16.14.0)
       ref
 
-GIT
-  remote: https://github.com/SamSaffron/vestal_versions
-  revision: 007b30a5274db7db55da745a4482243559247782
-  specs:
-    vestal_versions (1.2.3)
-      activerecord (> 3.0)
-      activesupport (> 3.0)
-
 GIT
   remote: https://github.com/callahad/omniauth-browserid.git
   revision: af62d667626c1622de6fe13b60849c3640765ab1
@@ -129,7 +121,6 @@ GEM
     daemons (1.1.9)
     debug_inspector (0.0.2)
     diff-lcs (1.2.4)
-    diffy (3.0.1)
     ember-data-source (0.14)
       ember-source
     ember-rails (0.14.1)
@@ -451,7 +442,6 @@ DEPENDENCIES
   better_errors
   binding_of_caller
   certified
-  diffy (>= 3.0)
   discourse_plugin!
   email_reply_parser!
   ember-rails
@@ -532,4 +522,3 @@ DEPENDENCIES
   uglifier
   unf
   unicorn
-  vestal_versions!
diff --git a/app/assets/javascripts/defer/google_diff_match_patch.js b/app/assets/javascripts/defer/google_diff_match_patch.js
deleted file mode 100644
index 810a8fa3679..00000000000
--- a/app/assets/javascripts/defer/google_diff_match_patch.js
+++ /dev/null
@@ -1,1378 +0,0 @@
-/**
- * Diff Match and Patch
- *
- * Copyright 2006 Google Inc.
- * http://code.google.com/p/google-diff-match-patch/
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * @fileoverview Computes the difference between two texts to create a patch.
- * Applies the patch onto another text, allowing for errors.
- * @author fraser@google.com (Neil Fraser)
- */
-
-/**
- * Class containing the diff, match and patch methods.
- * @constructor
- */
-function diff_match_patch() {
-  // Defaults.
-  // Redefine these in your program to override the defaults.
-
-  // Number of seconds to map a diff before giving up (0 for infinity).
-  this.Diff_Timeout = 1.0;
-  // Cost of an empty edit operation in terms of edit characters.
-  this.Diff_EditCost = 4;
-}
-
-
-//  DIFF FUNCTIONS
-
-
-/**
- * The data structure representing a diff is an array of tuples:
- * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
- * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
- */
-var DIFF_DELETE = -1;
-var DIFF_INSERT = 1;
-var DIFF_EQUAL = 0;
-
-/** @typedef {{0: number, 1: string}} */
-diff_match_patch.Diff;
-
-
-/**
- * Find the differences between two texts.  Simplifies the problem by stripping
- * any common prefix or suffix off the texts before diffing.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {boolean=} opt_checklines Optional speedup flag. If present and false,
- *     then don't run a line-level diff first to identify the changed areas.
- *     Defaults to true, which does a faster, slightly less optimal diff.
- * @param {number} opt_deadline Optional time when the diff should be complete
- *     by.  Used internally for recursive calls.  Users should set DiffTimeout
- *     instead.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- */
-diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines,
-    opt_deadline) {
-  // Set a deadline by which time the diff must be complete.
-  if (typeof opt_deadline == 'undefined') {
-    if (this.Diff_Timeout <= 0) {
-      opt_deadline = Number.MAX_VALUE;
-    } else {
-      opt_deadline = (new Date).getTime() + this.Diff_Timeout * 1000;
-    }
-  }
-  var deadline = opt_deadline;
-
-  // Check for null inputs.
-  if (text1 == null || text2 == null) {
-    throw new Error('Null input. (diff_main)');
-  }
-
-  // Check for equality (speedup).
-  if (text1 == text2) {
-    if (text1) {
-      return [[DIFF_EQUAL, text1]];
-    }
-    return [];
-  }
-
-  if (typeof opt_checklines == 'undefined') {
-    opt_checklines = true;
-  }
-  var checklines = opt_checklines;
-
-  // Trim off common prefix (speedup).
-  var commonlength = this.diff_commonPrefix(text1, text2);
-  var commonprefix = text1.substring(0, commonlength);
-  text1 = text1.substring(commonlength);
-  text2 = text2.substring(commonlength);
-
-  // Trim off common suffix (speedup).
-  commonlength = this.diff_commonSuffix(text1, text2);
-  var commonsuffix = text1.substring(text1.length - commonlength);
-  text1 = text1.substring(0, text1.length - commonlength);
-  text2 = text2.substring(0, text2.length - commonlength);
-
-  // Compute the diff on the middle block.
-  var diffs = this.diff_compute_(text1, text2, checklines, deadline);
-
-  // Restore the prefix and suffix.
-  if (commonprefix) {
-    diffs.unshift([DIFF_EQUAL, commonprefix]);
-  }
-  if (commonsuffix) {
-    diffs.push([DIFF_EQUAL, commonsuffix]);
-  }
-  this.diff_cleanupMerge(diffs);
-  return diffs;
-};
-
-
-/**
- * Find the differences between two texts.  Assumes that the texts do not
- * have any common prefix or suffix.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {boolean} checklines Speedup flag.  If false, then don't run a
- *     line-level diff first to identify the changed areas.
- *     If true, then run a faster, slightly less optimal diff.
- * @param {number} deadline Time when the diff should be complete by.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @private
- */
-diff_match_patch.prototype.diff_compute_ = function(text1, text2, checklines,
-    deadline) {
-  var diffs;
-
-  if (!text1) {
-    // Just add some text (speedup).
-    return [[DIFF_INSERT, text2]];
-  }
-
-  if (!text2) {
-    // Just delete some text (speedup).
-    return [[DIFF_DELETE, text1]];
-  }
-
-  var longtext = text1.length > text2.length ? text1 : text2;
-  var shorttext = text1.length > text2.length ? text2 : text1;
-  var i = longtext.indexOf(shorttext);
-  if (i != -1) {
-    // Shorter text is inside the longer text (speedup).
-    diffs = [[DIFF_INSERT, longtext.substring(0, i)],
-             [DIFF_EQUAL, shorttext],
-             [DIFF_INSERT, longtext.substring(i + shorttext.length)]];
-    // Swap insertions for deletions if diff is reversed.
-    if (text1.length > text2.length) {
-      diffs[0][0] = diffs[2][0] = DIFF_DELETE;
-    }
-    return diffs;
-  }
-
-  if (shorttext.length == 1) {
-    // Single character string.
-    // After the previous speedup, the character can't be an equality.
-    return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
-  }
-
-  // Check to see if the problem can be split in two.
-  var hm = this.diff_halfMatch_(text1, text2);
-  if (hm) {
-    // A half-match was found, sort out the return data.
-    var text1_a = hm[0];
-    var text1_b = hm[1];
-    var text2_a = hm[2];
-    var text2_b = hm[3];
-    var mid_common = hm[4];
-    // Send both pairs off for separate processing.
-    var diffs_a = this.diff_main(text1_a, text2_a, checklines, deadline);
-    var diffs_b = this.diff_main(text1_b, text2_b, checklines, deadline);
-    // Merge the results.
-    return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b);
-  }
-
-  if (checklines && text1.length > 100 && text2.length > 100) {
-    return this.diff_lineMode_(text1, text2, deadline);
-  }
-
-  return this.diff_bisect_(text1, text2, deadline);
-};
-
-
-/**
- * Do a quick line-level diff on both strings, then rediff the parts for
- * greater accuracy.
- * This speedup can produce non-minimal diffs.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {number} deadline Time when the diff should be complete by.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @private
- */
-diff_match_patch.prototype.diff_lineMode_ = function(text1, text2, deadline) {
-  // Scan the text on a line-by-line basis first.
-  var a = this.diff_linesToChars_(text1, text2);
-  text1 = a.chars1;
-  text2 = a.chars2;
-  var linearray = a.lineArray;
-
-  var diffs = this.diff_main(text1, text2, false, deadline);
-
-  // Convert the diff back to original text.
-  this.diff_charsToLines_(diffs, linearray);
-  // Eliminate freak matches (e.g. blank lines)
-  this.diff_cleanupSemantic(diffs);
-
-  // Rediff any replacement blocks, this time character-by-character.
-  // Add a dummy entry at the end.
-  diffs.push([DIFF_EQUAL, '']);
-  var pointer = 0;
-  var count_delete = 0;
-  var count_insert = 0;
-  var text_delete = '';
-  var text_insert = '';
-  while (pointer < diffs.length) {
-    switch (diffs[pointer][0]) {
-      case DIFF_INSERT:
-        count_insert++;
-        text_insert += diffs[pointer][1];
-        break;
-      case DIFF_DELETE:
-        count_delete++;
-        text_delete += diffs[pointer][1];
-        break;
-      case DIFF_EQUAL:
-        // Upon reaching an equality, check for prior redundancies.
-        if (count_delete >= 1 && count_insert >= 1) {
-          // Delete the offending records and add the merged ones.
-          diffs.splice(pointer - count_delete - count_insert,
-                       count_delete + count_insert);
-          pointer = pointer - count_delete - count_insert;
-          var a = this.diff_main(text_delete, text_insert, false, deadline);
-          for (var j = a.length - 1; j >= 0; j--) {
-            diffs.splice(pointer, 0, a[j]);
-          }
-          pointer = pointer + a.length;
-        }
-        count_insert = 0;
-        count_delete = 0;
-        text_delete = '';
-        text_insert = '';
-        break;
-    }
-    pointer++;
-  }
-  diffs.pop();  // Remove the dummy entry at the end.
-
-  return diffs;
-};
-
-
-/**
- * Find the 'middle snake' of a diff, split the problem in two
- * and return the recursively constructed diff.
- * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {number} deadline Time at which to bail if not yet complete.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @private
- */
-diff_match_patch.prototype.diff_bisect_ = function(text1, text2, deadline) {
-  // Cache the text lengths to prevent multiple calls.
-  var text1_length = text1.length;
-  var text2_length = text2.length;
-  var max_d = Math.ceil((text1_length + text2_length) / 2);
-  var v_offset = max_d;
-  var v_length = 2 * max_d;
-  var v1 = new Array(v_length);
-  var v2 = new Array(v_length);
-  // Setting all elements to -1 is faster in Chrome & Firefox than mixing
-  // integers and undefined.
-  for (var x = 0; x < v_length; x++) {
-    v1[x] = -1;
-    v2[x] = -1;
-  }
-  v1[v_offset + 1] = 0;
-  v2[v_offset + 1] = 0;
-  var delta = text1_length - text2_length;
-  // If the total number of characters is odd, then the front path will collide
-  // with the reverse path.
-  var front = (delta % 2 != 0);
-  // Offsets for start and end of k loop.
-  // Prevents mapping of space beyond the grid.
-  var k1start = 0;
-  var k1end = 0;
-  var k2start = 0;
-  var k2end = 0;
-  for (var d = 0; d < max_d; d++) {
-    // Bail out if deadline is reached.
-    if ((new Date()).getTime() > deadline) {
-      break;
-    }
-
-    // Walk the front path one step.
-    for (var k1 = -d + k1start; k1 <= d - k1end; k1 += 2) {
-      var k1_offset = v_offset + k1;
-      var x1;
-      if (k1 == -d || (k1 != d && v1[k1_offset - 1] < v1[k1_offset + 1])) {
-        x1 = v1[k1_offset + 1];
-      } else {
-        x1 = v1[k1_offset - 1] + 1;
-      }
-      var y1 = x1 - k1;
-      while (x1 < text1_length && y1 < text2_length &&
-             text1.charAt(x1) == text2.charAt(y1)) {
-        x1++;
-        y1++;
-      }
-      v1[k1_offset] = x1;
-      if (x1 > text1_length) {
-        // Ran off the right of the graph.
-        k1end += 2;
-      } else if (y1 > text2_length) {
-        // Ran off the bottom of the graph.
-        k1start += 2;
-      } else if (front) {
-        var k2_offset = v_offset + delta - k1;
-        if (k2_offset >= 0 && k2_offset < v_length && v2[k2_offset] != -1) {
-          // Mirror x2 onto top-left coordinate system.
-          var x2 = text1_length - v2[k2_offset];
-          if (x1 >= x2) {
-            // Overlap detected.
-            return this.diff_bisectSplit_(text1, text2, x1, y1, deadline);
-          }
-        }
-      }
-    }
-
-    // Walk the reverse path one step.
-    for (var k2 = -d + k2start; k2 <= d - k2end; k2 += 2) {
-      var k2_offset = v_offset + k2;
-      var x2;
-      if (k2 == -d || (k2 != d && v2[k2_offset - 1] < v2[k2_offset + 1])) {
-        x2 = v2[k2_offset + 1];
-      } else {
-        x2 = v2[k2_offset - 1] + 1;
-      }
-      var y2 = x2 - k2;
-      while (x2 < text1_length && y2 < text2_length &&
-             text1.charAt(text1_length - x2 - 1) ==
-             text2.charAt(text2_length - y2 - 1)) {
-        x2++;
-        y2++;
-      }
-      v2[k2_offset] = x2;
-      if (x2 > text1_length) {
-        // Ran off the left of the graph.
-        k2end += 2;
-      } else if (y2 > text2_length) {
-        // Ran off the top of the graph.
-        k2start += 2;
-      } else if (!front) {
-        var k1_offset = v_offset + delta - k2;
-        if (k1_offset >= 0 && k1_offset < v_length && v1[k1_offset] != -1) {
-          var x1 = v1[k1_offset];
-          var y1 = v_offset + x1 - k1_offset;
-          // Mirror x2 onto top-left coordinate system.
-          x2 = text1_length - x2;
-          if (x1 >= x2) {
-            // Overlap detected.
-            return this.diff_bisectSplit_(text1, text2, x1, y1, deadline);
-          }
-        }
-      }
-    }
-  }
-  // Diff took too long and hit the deadline or
-  // number of diffs equals number of characters, no commonality at all.
-  return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
-};
-
-
-/**
- * Given the location of the 'middle snake', split the diff in two parts
- * and recurse.
- * @param {string} text1 Old string to be diffed.
- * @param {string} text2 New string to be diffed.
- * @param {number} x Index of split point in text1.
- * @param {number} y Index of split point in text2.
- * @param {number} deadline Time at which to bail if not yet complete.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @private
- */
-diff_match_patch.prototype.diff_bisectSplit_ = function(text1, text2, x, y,
-    deadline) {
-  var text1a = text1.substring(0, x);
-  var text2a = text2.substring(0, y);
-  var text1b = text1.substring(x);
-  var text2b = text2.substring(y);
-
-  // Compute both diffs serially.
-  var diffs = this.diff_main(text1a, text2a, false, deadline);
-  var diffsb = this.diff_main(text1b, text2b, false, deadline);
-
-  return diffs.concat(diffsb);
-};
-
-
-/**
- * Split two texts into an array of strings.  Reduce the texts to a string of
- * hashes where each Unicode character represents one line.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {{chars1: string, chars2: string, lineArray: !Array.<string>}}
- *     An object containing the encoded text1, the encoded text2 and
- *     the array of unique strings.
- *     The zeroth element of the array of unique strings is intentionally blank.
- * @private
- */
-diff_match_patch.prototype.diff_linesToChars_ = function(text1, text2) {
-  var lineArray = [];  // e.g. lineArray[4] == 'Hello\n'
-  var lineHash = {};   // e.g. lineHash['Hello\n'] == 4
-
-  // '\x00' is a valid character, but various debuggers don't like it.
-  // So we'll insert a junk entry to avoid generating a null character.
-  lineArray[0] = '';
-
-  /**
-   * Split a text into an array of strings.  Reduce the texts to a string of
-   * hashes where each Unicode character represents one line.
-   * Modifies linearray and linehash through being a closure.
-   * @param {string} text String to encode.
-   * @return {string} Encoded string.
-   * @private
-   */
-  function diff_linesToCharsMunge_(text) {
-    var chars = '';
-    // Walk the text, pulling out a substring for each line.
-    // text.split('\n') would would temporarily double our memory footprint.
-    // Modifying text would create many large strings to garbage collect.
-    var lineStart = 0;
-    var lineEnd = -1;
-    // Keeping our own length variable is faster than looking it up.
-    var lineArrayLength = lineArray.length;
-    while (lineEnd < text.length - 1) {
-      lineEnd = text.indexOf('\n', lineStart);
-      if (lineEnd == -1) {
-        lineEnd = text.length - 1;
-      }
-      var line = text.substring(lineStart, lineEnd + 1);
-      lineStart = lineEnd + 1;
-
-      if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) :
-          (lineHash[line] !== undefined)) {
-        chars += String.fromCharCode(lineHash[line]);
-      } else {
-        chars += String.fromCharCode(lineArrayLength);
-        lineHash[line] = lineArrayLength;
-        lineArray[lineArrayLength++] = line;
-      }
-    }
-    return chars;
-  }
-
-  var chars1 = diff_linesToCharsMunge_(text1);
-  var chars2 = diff_linesToCharsMunge_(text2);
-  return {chars1: chars1, chars2: chars2, lineArray: lineArray};
-};
-
-
-/**
- * Rehydrate the text in a diff from a string of line hashes to real lines of
- * text.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @param {!Array.<string>} lineArray Array of unique strings.
- * @private
- */
-diff_match_patch.prototype.diff_charsToLines_ = function(diffs, lineArray) {
-  for (var x = 0; x < diffs.length; x++) {
-    var chars = diffs[x][1];
-    var text = [];
-    for (var y = 0; y < chars.length; y++) {
-      text[y] = lineArray[chars.charCodeAt(y)];
-    }
-    diffs[x][1] = text.join('');
-  }
-};
-
-
-/**
- * Determine the common prefix of two strings.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {number} The number of characters common to the start of each
- *     string.
- */
-diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) {
-  // Quick check for common null cases.
-  if (!text1 || !text2 || text1.charAt(0) != text2.charAt(0)) {
-    return 0;
-  }
-  // Binary search.
-  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
-  var pointermin = 0;
-  var pointermax = Math.min(text1.length, text2.length);
-  var pointermid = pointermax;
-  var pointerstart = 0;
-  while (pointermin < pointermid) {
-    if (text1.substring(pointerstart, pointermid) ==
-        text2.substring(pointerstart, pointermid)) {
-      pointermin = pointermid;
-      pointerstart = pointermin;
-    } else {
-      pointermax = pointermid;
-    }
-    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
-  }
-  return pointermid;
-};
-
-
-/**
- * Determine the common suffix of two strings.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {number} The number of characters common to the end of each string.
- */
-diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) {
-  // Quick check for common null cases.
-  if (!text1 || !text2 ||
-      text1.charAt(text1.length - 1) != text2.charAt(text2.length - 1)) {
-    return 0;
-  }
-  // Binary search.
-  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
-  var pointermin = 0;
-  var pointermax = Math.min(text1.length, text2.length);
-  var pointermid = pointermax;
-  var pointerend = 0;
-  while (pointermin < pointermid) {
-    if (text1.substring(text1.length - pointermid, text1.length - pointerend) ==
-        text2.substring(text2.length - pointermid, text2.length - pointerend)) {
-      pointermin = pointermid;
-      pointerend = pointermin;
-    } else {
-      pointermax = pointermid;
-    }
-    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
-  }
-  return pointermid;
-};
-
-
-/**
- * Determine if the suffix of one string is the prefix of another.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {number} The number of characters common to the end of the first
- *     string and the start of the second string.
- * @private
- */
-diff_match_patch.prototype.diff_commonOverlap_ = function(text1, text2) {
-  // Cache the text lengths to prevent multiple calls.
-  var text1_length = text1.length;
-  var text2_length = text2.length;
-  // Eliminate the null case.
-  if (text1_length == 0 || text2_length == 0) {
-    return 0;
-  }
-  // Truncate the longer string.
-  if (text1_length > text2_length) {
-    text1 = text1.substring(text1_length - text2_length);
-  } else if (text1_length < text2_length) {
-    text2 = text2.substring(0, text1_length);
-  }
-  var text_length = Math.min(text1_length, text2_length);
-  // Quick check for the worst case.
-  if (text1 == text2) {
-    return text_length;
-  }
-
-  // Start by looking for a single character match
-  // and increase length until no match is found.
-  // Performance analysis: http://neil.fraser.name/news/2010/11/04/
-  var best = 0;
-  var length = 1;
-  while (true) {
-    var pattern = text1.substring(text_length - length);
-    var found = text2.indexOf(pattern);
-    if (found == -1) {
-      return best;
-    }
-    length += found;
-    if (found == 0 || text1.substring(text_length - length) ==
-        text2.substring(0, length)) {
-      best = length;
-      length++;
-    }
-  }
-};
-
-
-/**
- * Do the two texts share a substring which is at least half the length of the
- * longer text?
- * This speedup can produce non-minimal diffs.
- * @param {string} text1 First string.
- * @param {string} text2 Second string.
- * @return {Array.<string>} Five element Array, containing the prefix of
- *     text1, the suffix of text1, the prefix of text2, the suffix of
- *     text2 and the common middle.  Or null if there was no match.
- * @private
- */
-diff_match_patch.prototype.diff_halfMatch_ = function(text1, text2) {
-  if (this.Diff_Timeout <= 0) {
-    // Don't risk returning a non-optimal diff if we have unlimited time.
-    return null;
-  }
-  var longtext = text1.length > text2.length ? text1 : text2;
-  var shorttext = text1.length > text2.length ? text2 : text1;
-  if (longtext.length < 4 || shorttext.length * 2 < longtext.length) {
-    return null;  // Pointless.
-  }
-  var dmp = this;  // 'this' becomes 'window' in a closure.
-
-  /**
-   * Does a substring of shorttext exist within longtext such that the substring
-   * is at least half the length of longtext?
-   * Closure, but does not reference any external variables.
-   * @param {string} longtext Longer string.
-   * @param {string} shorttext Shorter string.
-   * @param {number} i Start index of quarter length substring within longtext.
-   * @return {Array.<string>} Five element Array, containing the prefix of
-   *     longtext, the suffix of longtext, the prefix of shorttext, the suffix
-   *     of shorttext and the common middle.  Or null if there was no match.
-   * @private
-   */
-  function diff_halfMatchI_(longtext, shorttext, i) {
-    // Start with a 1/4 length substring at position i as a seed.
-    var seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
-    var j = -1;
-    var best_common = '';
-    var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b;
-    while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
-      var prefixLength = dmp.diff_commonPrefix(longtext.substring(i),
-                                               shorttext.substring(j));
-      var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i),
-                                               shorttext.substring(0, j));
-      if (best_common.length < suffixLength + prefixLength) {
-        best_common = shorttext.substring(j - suffixLength, j) +
-            shorttext.substring(j, j + prefixLength);
-        best_longtext_a = longtext.substring(0, i - suffixLength);
-        best_longtext_b = longtext.substring(i + prefixLength);
-        best_shorttext_a = shorttext.substring(0, j - suffixLength);
-        best_shorttext_b = shorttext.substring(j + prefixLength);
-      }
-    }
-    if (best_common.length * 2 >= longtext.length) {
-      return [best_longtext_a, best_longtext_b,
-              best_shorttext_a, best_shorttext_b, best_common];
-    } else {
-      return null;
-    }
-  }
-
-  // First check if the second quarter is the seed for a half-match.
-  var hm1 = diff_halfMatchI_(longtext, shorttext,
-                             Math.ceil(longtext.length / 4));
-  // Check again based on the third quarter.
-  var hm2 = diff_halfMatchI_(longtext, shorttext,
-                             Math.ceil(longtext.length / 2));
-  var hm;
-  if (!hm1 && !hm2) {
-    return null;
-  } else if (!hm2) {
-    hm = hm1;
-  } else if (!hm1) {
-    hm = hm2;
-  } else {
-    // Both matched.  Select the longest.
-    hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
-  }
-
-  // A half-match was found, sort out the return data.
-  var text1_a, text1_b, text2_a, text2_b;
-  if (text1.length > text2.length) {
-    text1_a = hm[0];
-    text1_b = hm[1];
-    text2_a = hm[2];
-    text2_b = hm[3];
-  } else {
-    text2_a = hm[0];
-    text2_b = hm[1];
-    text1_a = hm[2];
-    text1_b = hm[3];
-  }
-  var mid_common = hm[4];
-  return [text1_a, text1_b, text2_a, text2_b, mid_common];
-};
-
-
-/**
- * Reduce the number of edits by eliminating semantically trivial equalities.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- */
-diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) {
-  var changes = false;
-  var equalities = [];  // Stack of indices where equalities are found.
-  var equalitiesLength = 0;  // Keeping our own length var is faster in JS.
-  /** @type {?string} */
-  var lastequality = null;
-  // Always equal to diffs[equalities[equalitiesLength - 1]][1]
-  var pointer = 0;  // Index of current position.
-  // Number of characters that changed prior to the equality.
-  var length_insertions1 = 0;
-  var length_deletions1 = 0;
-  // Number of characters that changed after the equality.
-  var length_insertions2 = 0;
-  var length_deletions2 = 0;
-  while (pointer < diffs.length) {
-    if (diffs[pointer][0] == DIFF_EQUAL) {  // Equality found.
-      equalities[equalitiesLength++] = pointer;
-      length_insertions1 = length_insertions2;
-      length_deletions1 = length_deletions2;
-      length_insertions2 = 0;
-      length_deletions2 = 0;
-      lastequality = diffs[pointer][1];
-    } else {  // An insertion or deletion.
-      if (diffs[pointer][0] == DIFF_INSERT) {
-        length_insertions2 += diffs[pointer][1].length;
-      } else {
-        length_deletions2 += diffs[pointer][1].length;
-      }
-      // Eliminate an equality that is smaller or equal to the edits on both
-      // sides of it.
-      if (lastequality && (lastequality.length <=
-          Math.max(length_insertions1, length_deletions1)) &&
-          (lastequality.length <= Math.max(length_insertions2,
-                                           length_deletions2))) {
-        // Duplicate record.
-        diffs.splice(equalities[equalitiesLength - 1], 0,
-                     [DIFF_DELETE, lastequality]);
-        // Change second copy to insert.
-        diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
-        // Throw away the equality we just deleted.
-        equalitiesLength--;
-        // Throw away the previous equality (it needs to be reevaluated).
-        equalitiesLength--;
-        pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1;
-        length_insertions1 = 0;  // Reset the counters.
-        length_deletions1 = 0;
-        length_insertions2 = 0;
-        length_deletions2 = 0;
-        lastequality = null;
-        changes = true;
-      }
-    }
-    pointer++;
-  }
-
-  // Normalize the diff.
-  if (changes) {
-    this.diff_cleanupMerge(diffs);
-  }
-  this.diff_cleanupSemanticLossless(diffs);
-
-  // Find any overlaps between deletions and insertions.
-  // e.g: <del>abcxxx</del><ins>xxxdef</ins>
-  //   -> <del>abc</del>xxx<ins>def</ins>
-  // e.g: <del>xxxabc</del><ins>defxxx</ins>
-  //   -> <ins>def</ins>xxx<del>abc</del>
-  // Only extract an overlap if it is as big as the edit ahead or behind it.
-  pointer = 1;
-  while (pointer < diffs.length) {
-    if (diffs[pointer - 1][0] == DIFF_DELETE &&
-        diffs[pointer][0] == DIFF_INSERT) {
-      var deletion = diffs[pointer - 1][1];
-      var insertion = diffs[pointer][1];
-      var overlap_length1 = this.diff_commonOverlap_(deletion, insertion);
-      var overlap_length2 = this.diff_commonOverlap_(insertion, deletion);
-      if (overlap_length1 >= overlap_length2) {
-        if (overlap_length1 >= deletion.length / 2 ||
-            overlap_length1 >= insertion.length / 2) {
-          // Overlap found.  Insert an equality and trim the surrounding edits.
-          diffs.splice(pointer, 0,
-              [DIFF_EQUAL, insertion.substring(0, overlap_length1)]);
-          diffs[pointer - 1][1] =
-              deletion.substring(0, deletion.length - overlap_length1);
-          diffs[pointer + 1][1] = insertion.substring(overlap_length1);
-          pointer++;
-        }
-      } else {
-        if (overlap_length2 >= deletion.length / 2 ||
-            overlap_length2 >= insertion.length / 2) {
-          // Reverse overlap found.
-          // Insert an equality and swap and trim the surrounding edits.
-          diffs.splice(pointer, 0,
-              [DIFF_EQUAL, deletion.substring(0, overlap_length2)]);
-          diffs[pointer - 1][0] = DIFF_INSERT;
-          diffs[pointer - 1][1] =
-              insertion.substring(0, insertion.length - overlap_length2);
-          diffs[pointer + 1][0] = DIFF_DELETE;
-          diffs[pointer + 1][1] =
-              deletion.substring(overlap_length2);
-          pointer++;
-        }
-      }
-      pointer++;
-    }
-    pointer++;
-  }
-};
-
-
-/**
- * Look for single edits surrounded on both sides by equalities
- * which can be shifted sideways to align the edit to a word boundary.
- * e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- */
-diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) {
-  /**
-   * Given two strings, compute a score representing whether the internal
-   * boundary falls on logical boundaries.
-   * Scores range from 6 (best) to 0 (worst).
-   * Closure, but does not reference any external variables.
-   * @param {string} one First string.
-   * @param {string} two Second string.
-   * @return {number} The score.
-   * @private
-   */
-  function diff_cleanupSemanticScore_(one, two) {
-    if (!one || !two) {
-      // Edges are the best.
-      return 6;
-    }
-
-    // Each port of this function behaves slightly differently due to
-    // subtle differences in each language's definition of things like
-    // 'whitespace'.  Since this function's purpose is largely cosmetic,
-    // the choice has been made to use each language's native features
-    // rather than force total conformity.
-    var char1 = one.charAt(one.length - 1);
-    var char2 = two.charAt(0);
-    var nonAlphaNumeric1 = char1.match(diff_match_patch.nonAlphaNumericRegex_);
-    var nonAlphaNumeric2 = char2.match(diff_match_patch.nonAlphaNumericRegex_);
-    var whitespace1 = nonAlphaNumeric1 &&
-        char1.match(diff_match_patch.whitespaceRegex_);
-    var whitespace2 = nonAlphaNumeric2 &&
-        char2.match(diff_match_patch.whitespaceRegex_);
-    var lineBreak1 = whitespace1 &&
-        char1.match(diff_match_patch.linebreakRegex_);
-    var lineBreak2 = whitespace2 &&
-        char2.match(diff_match_patch.linebreakRegex_);
-    var blankLine1 = lineBreak1 &&
-        one.match(diff_match_patch.blanklineEndRegex_);
-    var blankLine2 = lineBreak2 &&
-        two.match(diff_match_patch.blanklineStartRegex_);
-
-    if (blankLine1 || blankLine2) {
-      // Five points for blank lines.
-      return 5;
-    } else if (lineBreak1 || lineBreak2) {
-      // Four points for line breaks.
-      return 4;
-    } else if (nonAlphaNumeric1 && !whitespace1 && whitespace2) {
-      // Three points for end of sentences.
-      return 3;
-    } else if (whitespace1 || whitespace2) {
-      // Two points for whitespace.
-      return 2;
-    } else if (nonAlphaNumeric1 || nonAlphaNumeric2) {
-      // One point for non-alphanumeric.
-      return 1;
-    }
-    return 0;
-  }
-
-  var pointer = 1;
-  // Intentionally ignore the first and last element (don't need checking).
-  while (pointer < diffs.length - 1) {
-    if (diffs[pointer - 1][0] == DIFF_EQUAL &&
-        diffs[pointer + 1][0] == DIFF_EQUAL) {
-      // This is a single edit surrounded by equalities.
-      var equality1 = diffs[pointer - 1][1];
-      var edit = diffs[pointer][1];
-      var equality2 = diffs[pointer + 1][1];
-
-      // First, shift the edit as far left as possible.
-      var commonOffset = this.diff_commonSuffix(equality1, edit);
-      if (commonOffset) {
-        var commonString = edit.substring(edit.length - commonOffset);
-        equality1 = equality1.substring(0, equality1.length - commonOffset);
-        edit = commonString + edit.substring(0, edit.length - commonOffset);
-        equality2 = commonString + equality2;
-      }
-
-      // Second, step character by character right, looking for the best fit.
-      var bestEquality1 = equality1;
-      var bestEdit = edit;
-      var bestEquality2 = equality2;
-      var bestScore = diff_cleanupSemanticScore_(equality1, edit) +
-          diff_cleanupSemanticScore_(edit, equality2);
-      while (edit.charAt(0) === equality2.charAt(0)) {
-        equality1 += edit.charAt(0);
-        edit = edit.substring(1) + equality2.charAt(0);
-        equality2 = equality2.substring(1);
-        var score = diff_cleanupSemanticScore_(equality1, edit) +
-            diff_cleanupSemanticScore_(edit, equality2);
-        // The >= encourages trailing rather than leading whitespace on edits.
-        if (score >= bestScore) {
-          bestScore = score;
-          bestEquality1 = equality1;
-          bestEdit = edit;
-          bestEquality2 = equality2;
-        }
-      }
-
-      if (diffs[pointer - 1][1] != bestEquality1) {
-        // We have an improvement, save it back to the diff.
-        if (bestEquality1) {
-          diffs[pointer - 1][1] = bestEquality1;
-        } else {
-          diffs.splice(pointer - 1, 1);
-          pointer--;
-        }
-        diffs[pointer][1] = bestEdit;
-        if (bestEquality2) {
-          diffs[pointer + 1][1] = bestEquality2;
-        } else {
-          diffs.splice(pointer + 1, 1);
-          pointer--;
-        }
-      }
-    }
-    pointer++;
-  }
-};
-
-// Define some regex patterns for matching boundaries.
-diff_match_patch.nonAlphaNumericRegex_ = /[^a-zA-Z0-9]/;
-diff_match_patch.whitespaceRegex_ = /\s/;
-diff_match_patch.linebreakRegex_ = /[\r\n]/;
-diff_match_patch.blanklineEndRegex_ = /\n\r?\n$/;
-diff_match_patch.blanklineStartRegex_ = /^\r?\n\r?\n/;
-
-/**
- * Reduce the number of edits by eliminating operationally trivial equalities.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- */
-diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) {
-  var changes = false;
-  var equalities = [];  // Stack of indices where equalities are found.
-  var equalitiesLength = 0;  // Keeping our own length var is faster in JS.
-  /** @type {?string} */
-  var lastequality = null;
-  // Always equal to diffs[equalities[equalitiesLength - 1]][1]
-  var pointer = 0;  // Index of current position.
-  // Is there an insertion operation before the last equality.
-  var pre_ins = false;
-  // Is there a deletion operation before the last equality.
-  var pre_del = false;
-  // Is there an insertion operation after the last equality.
-  var post_ins = false;
-  // Is there a deletion operation after the last equality.
-  var post_del = false;
-  while (pointer < diffs.length) {
-    if (diffs[pointer][0] == DIFF_EQUAL) {  // Equality found.
-      if (diffs[pointer][1].length < this.Diff_EditCost &&
-          (post_ins || post_del)) {
-        // Candidate found.
-        equalities[equalitiesLength++] = pointer;
-        pre_ins = post_ins;
-        pre_del = post_del;
-        lastequality = diffs[pointer][1];
-      } else {
-        // Not a candidate, and can never become one.
-        equalitiesLength = 0;
-        lastequality = null;
-      }
-      post_ins = post_del = false;
-    } else {  // An insertion or deletion.
-      if (diffs[pointer][0] == DIFF_DELETE) {
-        post_del = true;
-      } else {
-        post_ins = true;
-      }
-      /*
-       * Five types to be split:
-       * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
-       * <ins>A</ins>X<ins>C</ins><del>D</del>
-       * <ins>A</ins><del>B</del>X<ins>C</ins>
-       * <ins>A</del>X<ins>C</ins><del>D</del>
-       * <ins>A</ins><del>B</del>X<del>C</del>
-       */
-      if (lastequality && ((pre_ins && pre_del && post_ins && post_del) ||
-                           ((lastequality.length < this.Diff_EditCost / 2) &&
-                            (pre_ins + pre_del + post_ins + post_del) == 3))) {
-        // Duplicate record.
-        diffs.splice(equalities[equalitiesLength - 1], 0,
-                     [DIFF_DELETE, lastequality]);
-        // Change second copy to insert.
-        diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
-        equalitiesLength--;  // Throw away the equality we just deleted;
-        lastequality = null;
-        if (pre_ins && pre_del) {
-          // No changes made which could affect previous entry, keep going.
-          post_ins = post_del = true;
-          equalitiesLength = 0;
-        } else {
-          equalitiesLength--;  // Throw away the previous equality.
-          pointer = equalitiesLength > 0 ?
-              equalities[equalitiesLength - 1] : -1;
-          post_ins = post_del = false;
-        }
-        changes = true;
-      }
-    }
-    pointer++;
-  }
-
-  if (changes) {
-    this.diff_cleanupMerge(diffs);
-  }
-};
-
-
-/**
- * Reorder and merge like edit sections.  Merge equalities.
- * Any edit section can move as long as it doesn't cross an equality.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- */
-diff_match_patch.prototype.diff_cleanupMerge = function(diffs) {
-  diffs.push([DIFF_EQUAL, '']);  // Add a dummy entry at the end.
-  var pointer = 0;
-  var count_delete = 0;
-  var count_insert = 0;
-  var text_delete = '';
-  var text_insert = '';
-  var commonlength;
-  while (pointer < diffs.length) {
-    switch (diffs[pointer][0]) {
-      case DIFF_INSERT:
-        count_insert++;
-        text_insert += diffs[pointer][1];
-        pointer++;
-        break;
-      case DIFF_DELETE:
-        count_delete++;
-        text_delete += diffs[pointer][1];
-        pointer++;
-        break;
-      case DIFF_EQUAL:
-        // Upon reaching an equality, check for prior redundancies.
-        if (count_delete + count_insert > 1) {
-          if (count_delete !== 0 && count_insert !== 0) {
-            // Factor out any common prefixies.
-            commonlength = this.diff_commonPrefix(text_insert, text_delete);
-            if (commonlength !== 0) {
-              if ((pointer - count_delete - count_insert) > 0 &&
-                  diffs[pointer - count_delete - count_insert - 1][0] ==
-                  DIFF_EQUAL) {
-                diffs[pointer - count_delete - count_insert - 1][1] +=
-                    text_insert.substring(0, commonlength);
-              } else {
-                diffs.splice(0, 0, [DIFF_EQUAL,
-                                    text_insert.substring(0, commonlength)]);
-                pointer++;
-              }
-              text_insert = text_insert.substring(commonlength);
-              text_delete = text_delete.substring(commonlength);
-            }
-            // Factor out any common suffixies.
-            commonlength = this.diff_commonSuffix(text_insert, text_delete);
-            if (commonlength !== 0) {
-              diffs[pointer][1] = text_insert.substring(text_insert.length -
-                  commonlength) + diffs[pointer][1];
-              text_insert = text_insert.substring(0, text_insert.length -
-                  commonlength);
-              text_delete = text_delete.substring(0, text_delete.length -
-                  commonlength);
-            }
-          }
-          // Delete the offending records and add the merged ones.
-          if (count_delete === 0) {
-            diffs.splice(pointer - count_insert,
-                count_delete + count_insert, [DIFF_INSERT, text_insert]);
-          } else if (count_insert === 0) {
-            diffs.splice(pointer - count_delete,
-                count_delete + count_insert, [DIFF_DELETE, text_delete]);
-          } else {
-            diffs.splice(pointer - count_delete - count_insert,
-                count_delete + count_insert, [DIFF_DELETE, text_delete],
-                [DIFF_INSERT, text_insert]);
-          }
-          pointer = pointer - count_delete - count_insert +
-                    (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1;
-        } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) {
-          // Merge this equality with the previous one.
-          diffs[pointer - 1][1] += diffs[pointer][1];
-          diffs.splice(pointer, 1);
-        } else {
-          pointer++;
-        }
-        count_insert = 0;
-        count_delete = 0;
-        text_delete = '';
-        text_insert = '';
-        break;
-    }
-  }
-  if (diffs[diffs.length - 1][1] === '') {
-    diffs.pop();  // Remove the dummy entry at the end.
-  }
-
-  // Second pass: look for single edits surrounded on both sides by equalities
-  // which can be shifted sideways to eliminate an equality.
-  // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
-  var changes = false;
-  pointer = 1;
-  // Intentionally ignore the first and last element (don't need checking).
-  while (pointer < diffs.length - 1) {
-    if (diffs[pointer - 1][0] == DIFF_EQUAL &&
-        diffs[pointer + 1][0] == DIFF_EQUAL) {
-      // This is a single edit surrounded by equalities.
-      if (diffs[pointer][1].substring(diffs[pointer][1].length -
-          diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) {
-        // Shift the edit over the previous equality.
-        diffs[pointer][1] = diffs[pointer - 1][1] +
-            diffs[pointer][1].substring(0, diffs[pointer][1].length -
-                                        diffs[pointer - 1][1].length);
-        diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
-        diffs.splice(pointer - 1, 1);
-        changes = true;
-      } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length) ==
-          diffs[pointer + 1][1]) {
-        // Shift the edit over the next equality.
-        diffs[pointer - 1][1] += diffs[pointer + 1][1];
-        diffs[pointer][1] =
-            diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
-            diffs[pointer + 1][1];
-        diffs.splice(pointer + 1, 1);
-        changes = true;
-      }
-    }
-    pointer++;
-  }
-  // If shifts were made, the diff needs reordering and another shift sweep.
-  if (changes) {
-    this.diff_cleanupMerge(diffs);
-  }
-};
-
-
-/**
- * loc is a location in text1, compute and return the equivalent location in
- * text2.
- * e.g. 'The cat' vs 'The big cat', 1->1, 5->8
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @param {number} loc Location within text1.
- * @return {number} Location within text2.
- */
-diff_match_patch.prototype.diff_xIndex = function(diffs, loc) {
-  var chars1 = 0;
-  var chars2 = 0;
-  var last_chars1 = 0;
-  var last_chars2 = 0;
-  var x;
-  for (x = 0; x < diffs.length; x++) {
-    if (diffs[x][0] !== DIFF_INSERT) {  // Equality or deletion.
-      chars1 += diffs[x][1].length;
-    }
-    if (diffs[x][0] !== DIFF_DELETE) {  // Equality or insertion.
-      chars2 += diffs[x][1].length;
-    }
-    if (chars1 > loc) {  // Overshot the location.
-      break;
-    }
-    last_chars1 = chars1;
-    last_chars2 = chars2;
-  }
-  // Was the location was deleted?
-  if (diffs.length != x && diffs[x][0] === DIFF_DELETE) {
-    return last_chars2;
-  }
-  // Add the remaining character length.
-  return last_chars2 + (loc - last_chars1);
-};
-
-
-/**
- * Convert a diff array into a pretty HTML report.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {string} HTML representation.
- */
-diff_match_patch.prototype.diff_prettyHtml = function(diffs) {
-  var html = [];
-  for (var x = 0; x < diffs.length; x++) {
-    var op = diffs[x][0];    // Operation (insert, delete, equal)
-    var data = diffs[x][1];  // Text of change.
-    switch (op) {
-      case DIFF_INSERT:
-        html[x] = '<ins>' + data + '</ins>';
-        break;
-      case DIFF_DELETE:
-        html[x] = '<del>' + data + '</del>';
-        break;
-      case DIFF_EQUAL:
-        html[x] = data;
-        break;
-    }
-  }
-  return html.join('');
-};
-
-
-/**
- * Compute and return the source text (all equalities and deletions).
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {string} Source text.
- */
-diff_match_patch.prototype.diff_text1 = function(diffs) {
-  var text = [];
-  for (var x = 0; x < diffs.length; x++) {
-    if (diffs[x][0] !== DIFF_INSERT) {
-      text[x] = diffs[x][1];
-    }
-  }
-  return text.join('');
-};
-
-
-/**
- * Compute and return the destination text (all equalities and insertions).
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {string} Destination text.
- */
-diff_match_patch.prototype.diff_text2 = function(diffs) {
-  var text = [];
-  for (var x = 0; x < diffs.length; x++) {
-    if (diffs[x][0] !== DIFF_DELETE) {
-      text[x] = diffs[x][1];
-    }
-  }
-  return text.join('');
-};
-
-
-/**
- * Compute the Levenshtein distance; the number of inserted, deleted or
- * substituted characters.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {number} Number of changes.
- */
-diff_match_patch.prototype.diff_levenshtein = function(diffs) {
-  var levenshtein = 0;
-  var insertions = 0;
-  var deletions = 0;
-  for (var x = 0; x < diffs.length; x++) {
-    var op = diffs[x][0];
-    var data = diffs[x][1];
-    switch (op) {
-      case DIFF_INSERT:
-        insertions += data.length;
-        break;
-      case DIFF_DELETE:
-        deletions += data.length;
-        break;
-      case DIFF_EQUAL:
-        // A deletion and an insertion is one substitution.
-        levenshtein += Math.max(insertions, deletions);
-        insertions = 0;
-        deletions = 0;
-        break;
-    }
-  }
-  levenshtein += Math.max(insertions, deletions);
-  return levenshtein;
-};
-
-
-/**
- * Crush the diff into an encoded string which describes the operations
- * required to transform text1 into text2.
- * E.g. =3\t-2\t+ing  -> Keep 3 chars, delete 2 chars, insert 'ing'.
- * Operations are tab-separated.  Inserted text is escaped using %xx notation.
- * @param {!Array.<!diff_match_patch.Diff>} diffs Array of diff tuples.
- * @return {string} Delta text.
- */
-diff_match_patch.prototype.diff_toDelta = function(diffs) {
-  var text = [];
-  for (var x = 0; x < diffs.length; x++) {
-    switch (diffs[x][0]) {
-      case DIFF_INSERT:
-        text[x] = '+' + encodeURI(diffs[x][1]);
-        break;
-      case DIFF_DELETE:
-        text[x] = '-' + diffs[x][1].length;
-        break;
-      case DIFF_EQUAL:
-        text[x] = '=' + diffs[x][1].length;
-        break;
-    }
-  }
-  return text.join('\t').replace(/%20/g, ' ');
-};
-
-
-/**
- * Given the original text1, and an encoded string which describes the
- * operations required to transform text1 into text2, compute the full diff.
- * @param {string} text1 Source string for the diff.
- * @param {string} delta Delta text.
- * @return {!Array.<!diff_match_patch.Diff>} Array of diff tuples.
- * @throws {!Error} If invalid input.
- */
-diff_match_patch.prototype.diff_fromDelta = function(text1, delta) {
-  var diffs = [];
-  var diffsLength = 0;  // Keeping our own length var is faster in JS.
-  var pointer = 0;  // Cursor in text1
-  var tokens = delta.split(/\t/g);
-  for (var x = 0; x < tokens.length; x++) {
-    // Each token begins with a one character parameter which specifies the
-    // operation of this token (delete, insert, equality).
-    var param = tokens[x].substring(1);
-    switch (tokens[x].charAt(0)) {
-      case '+':
-        try {
-          diffs[diffsLength++] = [DIFF_INSERT, decodeURI(param)];
-        } catch (ex) {
-          // Malformed URI sequence.
-          throw new Error('Illegal escape in diff_fromDelta: ' + param);
-        }
-        break;
-      case '-':
-        // Fall through.
-      case '=':
-        var n = parseInt(param, 10);
-        if (isNaN(n) || n < 0) {
-          throw new Error('Invalid number in diff_fromDelta: ' + param);
-        }
-        var text = text1.substring(pointer, pointer += n);
-        if (tokens[x].charAt(0) == '=') {
-          diffs[diffsLength++] = [DIFF_EQUAL, text];
-        } else {
-          diffs[diffsLength++] = [DIFF_DELETE, text];
-        }
-        break;
-      default:
-        // Blank tokens are ok (from a trailing \t).
-        // Anything else is an error.
-        if (tokens[x]) {
-          throw new Error('Invalid diff operation in diff_fromDelta: ' +
-                          tokens[x]);
-        }
-    }
-  }
-  if (pointer != text1.length) {
-    throw new Error('Delta length (' + pointer +
-        ') does not equal source text length (' + text1.length + ').');
-  }
-  return diffs;
-};
-
-// Export these global variables so that they survive Google's JS compiler.
-// In a browser, 'this' will be 'window'.
-// Users of node.js should 'require' the uncompressed version since Google's
-// JS compiler may break the following exports for non-browser environments.
-this['diff_match_patch'] = diff_match_patch;
-this['DIFF_DELETE'] = DIFF_DELETE;
-this['DIFF_INSERT'] = DIFF_INSERT;
-this['DIFF_EQUAL'] = DIFF_EQUAL;
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/controllers/history_controller.js b/app/assets/javascripts/discourse/controllers/history_controller.js
index db797011906..65f89de6a91 100644
--- a/app/assets/javascripts/discourse/controllers/history_controller.js
+++ b/app/assets/javascripts/discourse/controllers/history_controller.js
@@ -1,6 +1,3 @@
-/*jshint newcap:false*/
-/*global diff_match_patch:true assetPath:true*/
-
 /**
   This controller handles displaying of history
 
@@ -11,79 +8,43 @@
   @module Discourse
 **/
 Discourse.HistoryController = Discourse.ObjectController.extend(Discourse.ModalFunctionality, {
-  diffLibraryLoaded: false,
-  diff: null,
+  loading: false,
+  viewMode: "side_by_side",
 
-  init: function(){
-    this._super();
-    var historyController = this;
-    $LAB.script(assetPath('defer/google_diff_match_patch')).wait(function(){
-      historyController.set('diffLibraryLoaded', true);
-    });
-  },
+  refresh: function(postId, postVersion) {
+    this.set("loading", true);
 
-  loadSide: function(side) {
-    if (this.get("version" + side)) {
-      var orig = this.get('model');
-      var version = this.get("version" + side + ".number");
-      if (version === orig.get('version')) {
-        this.set("post" + side, orig);
-      } else {
-        var historyController = this;
-        Discourse.Post.loadVersion(orig.get('id'), version).then(function(post) {
-          historyController.set("post" + side, post);
-        });
-      }
-    }
-  },
-
-  changedLeftVersion: function() {
-    this.loadSide("Left");
-  }.observes('versionLeft'),
-
-  changedRightVersion: function() {
-    this.loadSide("Right");
-  }.observes('versionRight'),
-
-  loadedPosts: function() {
-    if (this.get('diffLibraryLoaded') && this.get('postLeft') && this.get('postRight')) {
-      var dmp = new diff_match_patch(),
-          before = this.get("postLeft.cooked"),
-          after = this.get("postRight.cooked"),
-          diff = dmp.diff_main(before, after);
-      dmp.diff_cleanupSemantic(diff);
-      this.set('diff', dmp.diff_prettyHtml(diff));
-    }
-  }.observes('diffLibraryLoaded', 'postLeft', 'postRight'),
-
-  refresh: function() {
-    this.setProperties({
-      loading: true,
-      postLeft: null,
-      postRight: null
-    });
-
-    var historyController = this;
-    this.get('model').loadVersions().then(function(result) {
-      _.each(result,function(item) {
-
-        var age = Discourse.Formatter.relativeAge(new Date(item.created_at), {
-          format: 'medium',
-          leaveAgo: true,
-          wrapInSpan: false});
-
-        item.description = "v" + item.number + " - " + age + " - " + I18n.t("changed_by", { author: item.display_username });
-      });
-
-      historyController.setProperties({
+    var self = this;
+    Discourse.Post.loadRevision(postId, postVersion).then(function (result) {
+      self.setProperties({
         loading: false,
-        versionLeft: result[0],
-        versionRight: result[result.length-1],
-        versions: result
+        model: result
       });
     });
+  },
+
+  createdAtDate: function() { return moment(this.get("created_at")).format("LLLL"); }.property("created_at"),
+
+  previousVersionNumber: function() { return this.get("version") - 1; }.property("version"),
+  currentVersionNumber: Em.computed.alias("version"),
+
+  isFirstVersion: Em.computed.equal("version", 2),
+  isLastVersion: Discourse.computed.propertyEqual("version", "revisions_count"),
+
+  displayingInline: Em.computed.equal("viewMode", "inline"),
+  displayingSideBySide: Em.computed.equal("viewMode", "side_by_side"),
+  displayingSideBySideMarkdown: Em.computed.equal("viewMode", "side_by_side_markdown"),
+
+  diff: function() { return this.get(this.get("viewMode")); }.property("inline", "side_by_side", "side_by_side_markdown", "viewMode"),
+
+  actions: {
+    loadFirstVersion: function() { this.refresh(this.get("post_id"), 2); },
+    loadPreviousVersion: function() { this.refresh(this.get("post_id"), this.get("version") - 1); },
+    loadNextVersion: function() { this.refresh(this.get("post_id"), this.get("version") + 1); },
+    loadLastVersion: function() { this.refresh(this.get("post_id"), this.get("revisions_count")); },
+
+    displayInline: function() { this.set("viewMode", "inline"); },
+    displaySideBySide: function() { this.set("viewMode", "side_by_side"); },
+    displaySideBySideMarkdown: function() { this.set("viewMode", "side_by_side_markdown"); }
   }
-
 });
-
-
diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js
index 877f48e338a..521b076f850 100644
--- a/app/assets/javascripts/discourse/models/post.js
+++ b/app/assets/javascripts/discourse/models/post.js
@@ -337,10 +337,6 @@ Discourse.Post = Discourse.Model.extend({
     });
   },
 
-  loadVersions: function() {
-    return Discourse.ajax("/posts/" + (this.get('id')) + "/versions.json");
-  },
-
   // Whether to show replies directly below
   showRepliesBelow: function() {
     var reply_count = this.get('reply_count');
@@ -403,14 +399,14 @@ Discourse.Post.reopenClass({
     });
   },
 
-  loadVersion: function(postId, version, callback) {
-    return Discourse.ajax("/posts/" + postId + ".json?version=" + version).then(function(result) {
+  loadRevision: function(postId, version) {
+    return Discourse.ajax("/posts/" + postId + "/revisions/" + version + ".json").then(function (result) {
       return Discourse.Post.create(result);
     });
   },
 
   loadQuote: function(postId) {
-    return Discourse.ajax("/posts/" + postId + ".json").then(function(result) {
+    return Discourse.ajax("/posts/" + postId + ".json").then(function (result) {
       var post = Discourse.Post.create(result);
       return Discourse.Quote.build(post, post.get('raw'));
     });
diff --git a/app/assets/javascripts/discourse/routes/topic_route.js b/app/assets/javascripts/discourse/routes/topic_route.js
index 2dcc146fe75..5e922405387 100644
--- a/app/assets/javascripts/discourse/routes/topic_route.js
+++ b/app/assets/javascripts/discourse/routes/topic_route.js
@@ -49,7 +49,7 @@ Discourse.TopicRoute = Discourse.Route.extend({
 
     showHistory: function(post) {
       Discourse.Route.showModal(this, 'history', post);
-      this.controllerFor('history').refresh();
+      this.controllerFor('history').refresh(post.get("id"), post.get("version"));
       this.controllerFor('modal').set('modalClass', 'history-modal');
     },
 
diff --git a/app/assets/javascripts/discourse/templates/modal/history.js.handlebars b/app/assets/javascripts/discourse/templates/modal/history.js.handlebars
index 2a5bb997f33..a7683aa83d3 100644
--- a/app/assets/javascripts/discourse/templates/modal/history.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/modal/history.js.handlebars
@@ -1,48 +1,24 @@
 <div class="modal-body">
-
   {{#if loading}}
     {{i18n loading}}
   {{else}}
-    {{#if versions}}
-      <div class='span8'>
-
-        {{view Ember.Select
-          contentBinding="versions"
-          optionLabelPath="content.description"
-          optionValuePath="content.number"
-          selectionBinding="versionLeft"}}
-
-        <div class='contents'>
-          {{#if postLeft}}
-            {{{postLeft.cooked}}}
-          {{else}}
-            <div class='history-loading'>{{i18n loading}}</div>
-          {{/if}}
-        </div>
-
+    <div>
+      <div id="revision-controls">
+        <button class="btn standard" title="{{i18n post.revisions.controls.first}}" {{action loadFirstVersion}} {{bindAttr disabled=isFirstVersion}}><i class="fa fa-fast-backward"></i></button>
+        <button class="btn standard" title="{{i18n post.revisions.controls.previous}}" {{action loadPreviousVersion}} {{bindAttr disabled=isFirstVersion}}><i class="fa fa-backward"></i></button>
+        {{{i18n post.revisions.controls.comparing_previous_to_current_out_of_total previous=previousVersionNumber current=currentVersionNumber total=revisions_count}}}
+        <button class="btn standard" title="{{i18n post.revisions.controls.next}}" {{action loadNextVersion}} {{bindAttr disabled=isLastVersion}}><i class="fa fa-forward"></i></button>
+        <button class="btn standard" title="{{i18n post.revisions.controls.last}}" {{action loadLastVersion}} {{bindAttr disabled=isLastVersion}}><i class="fa fa-fast-forward"></i></button>
       </div>
-
-      <div class='span8 offset1'>
-        {{view Ember.Select
-          contentBinding="versions"
-          optionLabelPath="content.description"
-          optionValuePath="content.number"
-          selectionBinding="versionRight"}}
-
-        {{#if postRight.edit_reason}}
-          <p><strong>{{i18n post.edit_reason}}</strong>{{postRight.edit_reason}}</p>
-        {{/if}}
-
-        <div class='contents'>
-          {{#if diff}}
-            {{{diff}}}
-          {{else}}
-            <div class='history-loading'>{{i18n loading}}</div>
-          {{/if}}
-        </div>
-
+      <div id="display-modes">
+        <button {{bindAttr class=":btn displayingInline:btn-primary:standard"}} title="{{i18n post.revisions.displays.inline.title}}" {{action displayInline}}>{{{i18n post.revisions.displays.inline.button}}}</button>
+        <button {{bindAttr class=":btn displayingSideBySide:btn-primary:standard"}} title="{{i18n post.revisions.displays.side_by_side.title}}" {{action displaySideBySide}}>{{{i18n post.revisions.displays.side_by_side.button}}}</button>
+        <button {{bindAttr class=":btn displayingSideBySideMarkdown:btn-primary:standard"}} title="{{i18n post.revisions.displays.side_by_side_markdown.title}}" {{action displaySideBySideMarkdown}}>{{{i18n post.revisions.displays.side_by_side_markdown.button}}}</button>
       </div>
-    {{/if}}
+    </div>
+    <div id="revision-details">
+      {{i18n post.revisions.details.edited_by}} {{avatar this imageSize="small"}} {{username}} <span class="date">{{date path="created_at" leaveAgo="true"}}</span> {{#if edit_reason}} &mdash; <span class="edit-reason">{{edit_reason}}</span>{{/if}}
+    </div>
+    {{{diff}}}
   {{/if}}
-
 </div>
diff --git a/app/assets/javascripts/discourse/templates/user/stream.js.handlebars b/app/assets/javascripts/discourse/templates/user/stream.js.handlebars
index 9121e1908be..293ceb1013f 100644
--- a/app/assets/javascripts/discourse/templates/user/stream.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/user/stream.js.handlebars
@@ -14,9 +14,10 @@
     {{#groupedEach children}}
       <div class='child-actions'>
         <i class="icon {{unbound icon}}"></i>
-      {{#groupedEach items}}
-        <a href="{{unbound userUrl}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar this imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
-      {{/groupedEach}}
+        {{#groupedEach items}}
+          <a href="{{unbound userUrl}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar this imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
+          {{#if edit_reason}} &mdash; <span class="edit-reason">{{unbound edit_reason}}</span>{{/if}}
+        {{/groupedEach}}
       </div>
     {{/groupedEach}}
   </div>
diff --git a/app/assets/javascripts/discourse/views/modal/hide_modal_view.js b/app/assets/javascripts/discourse/views/modal/hide_modal_view.js
index 7844871cd33..172751b24a4 100644
--- a/app/assets/javascripts/discourse/views/modal/hide_modal_view.js
+++ b/app/assets/javascripts/discourse/views/modal/hide_modal_view.js
@@ -15,4 +15,4 @@ Discourse.HideModalView = Discourse.ModalBodyView.extend({
     $('#discourse-modal').modal('hide');
   }
 
-});
\ No newline at end of file
+});
diff --git a/app/assets/javascripts/discourse/views/modal/history_view.js b/app/assets/javascripts/discourse/views/modal/history_view.js
index 6416e71cede..c02c4dfd000 100644
--- a/app/assets/javascripts/discourse/views/modal/history_view.js
+++ b/app/assets/javascripts/discourse/views/modal/history_view.js
@@ -8,5 +8,11 @@
 **/
 Discourse.HistoryView = Discourse.ModalBodyView.extend({
   templateName: 'modal/history',
-  title: I18n.t('history')
+  title: I18n.t('history'),
+
+  resizeModal: function(){
+    var viewPortHeight = $(window).height();
+    this.$(".modal-body").css("max-height", Math.floor(0.8 * viewPortHeight) + "px");
+  }.on("didInsertElement")
+
 });
diff --git a/app/assets/javascripts/discourse/views/modal/modal_body_view.js b/app/assets/javascripts/discourse/views/modal/modal_body_view.js
index bfaceb5c586..a1d58f9fb66 100644
--- a/app/assets/javascripts/discourse/views/modal/modal_body_view.js
+++ b/app/assets/javascripts/discourse/views/modal/modal_body_view.js
@@ -10,14 +10,19 @@ Discourse.ModalBodyView = Discourse.View.extend({
 
   // Focus on first element
   didInsertElement: function() {
+    var self = this;
+
     $('#discourse-modal').modal('show');
 
+    $('#discourse-modal').one("hide", function () {
+      self.get("controller").send("closeModal");
+    });
+
     $('#modal-alert').hide();
 
     if (!Discourse.Mobile.mobileView) {
-      var modalBodyView = this;
       Em.run.schedule('afterRender', function() {
-        modalBodyView.$('input:first').focus();
+        self.$('input:first').focus();
       });
     }
 
diff --git a/app/assets/stylesheets/desktop/history.scss b/app/assets/stylesheets/desktop/history.scss
index c8d61556bdf..5355b2f1d76 100644
--- a/app/assets/stylesheets/desktop/history.scss
+++ b/app/assets/stylesheets/desktop/history.scss
@@ -1,5 +1,4 @@
-// styles that apply to the popup that appears when you show the edit history
-// of a post
+// styles that apply to the popup that appears when you show the edit history of a post
 
 @import "common/foundation/variables";
 @import "common/foundation/mixins";
@@ -9,30 +8,105 @@
     min-width: 960px;
     min-height: 500px;
   }
-
+  #revision-controls {
+    float: left;
+  }
+  #display-modes {
+    text-align: right;
+  }
+  #revision-details {
+    background-color: #eee;
+    padding: 5px;
+    margin-top: 10px;
+  }
+  img {
+    max-width: 670px;
+    height: auto;
+  }
+  .inline-diff {
+    width: 670px;
+    word-wrap: break-word;
+  }
+  .markdown {
+    word-wrap: break-word;
+    white-space: pre-wrap;
+    font-family: monospace;
+    font-size: 12px;
+    width: 100%;
+    border-collapse: collapse;
+    border-spacing: 0px;
+    td {
+      width: 50%;
+      vertical-align: top;
+    }
+  }
+  .span8, .markdown {
+    img {
+      max-width: 400px;
+    }
+  }
+  ins, .diff-ins {
+    code, img {
+      border: 2px solid #405A04;
+    }
+    img {
+      opacity: .75;
+      filter: alpha(opacity=75);
+    }
+    a {
+      color: #2D4003;
+      text-decoration: none;
+    }
+  }
+  img.diff-ins, code.diff-ins {
+    border: 2px solid #405A04;
+  }
+  img.diff-ins {
+    opacity: .75;
+    filter: alpha(opacity=75);
+  }
+  .diff-ins {
+    background: #f9ffe1;
+  }
   ins {
-    background: #e6ffe6;
+    color: #405A04;
+    background: #D1E1AD;
+  }
+  del, .diff-del {
+    code, img {
+      border: 2px solid #A82400;
+    }
+    img {
+      opacity: .5;
+      filter: alpha(opacity=50);
+    }
+    a {
+      color: #400E00;
+      text-decoration: none;
+    }
+  }
+  img.diff-del, code.diff-del {
+    border: 2px solid #A82400;
+  }
+  img.diff-del {
+    opacity: .5;
+    filter: alpha(opacity=50);
+  }
+  .diff-del {
+    background: #fff4f4;
   }
   del {
-    background: #ffe6e6;
+    color: #A82400;
+    background: #E5BDB2;
+  }
+  span.date {
+    font-weight: bold;
+  }
+  span.edit-reason {
+    background-color: #ffffcc;
+    padding: 3px 5px 5px 5px;
   }
   .modal-header {
     height: 42px;
   }
-  .history-loading {
-    margin: 25px 0;
-    width: 120px;
-    font-size: 20px;
-    padding: 8px 0 30px 30px;
-    background: {
-      image: image-url("spinner_96.gif");
-      repeat: no-repeat;
-      size: 25px 25px;
-      position: 0 4px;
-    };
-  }
-  select {
-    height: auto;
-	width: auto;
-  }
 }
diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss
index ca4093bed81..8d7040391ab 100644
--- a/app/assets/stylesheets/desktop/user.scss
+++ b/app/assets/stylesheets/desktop/user.scss
@@ -301,6 +301,10 @@
       margin-bottom: 4px;
       font-size: 14px;
     }
+    .edit-reason {
+      background-color: #ffffcc;
+      padding: 3px 5px 5px 5px;
+    }
   }
 }
 
diff --git a/app/assets/stylesheets/mobile/history.scss b/app/assets/stylesheets/mobile/history.scss
index c431b2680d4..86c4908aae0 100644
--- a/app/assets/stylesheets/mobile/history.scss
+++ b/app/assets/stylesheets/mobile/history.scss
@@ -21,26 +21,6 @@
     padding-bottom: 10px;
   }
   .modal-body {padding-top: 0px;}
-  .history-loading {
-    margin: 25px 0;
-    width: 120px;
-    font-size: 20px;
-    padding: 8px 0 30px 30px;
-    background: {
-      image: image-url("spinner_96.gif");
-      repeat: no-repeat;
-      size: 25px 25px;
-      position: 0 4px;
-    };
-  }
-  select {
-    height: auto;
-	width: auto;
-  font-size: 16px;
-  position: fixed;
-  margin-top: -45px;
-  background-color: #fff;
-  }
 }
 
 .offset1 {display: none;}
diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss
index fb517924873..77c90129a0e 100644
--- a/app/assets/stylesheets/mobile/user.scss
+++ b/app/assets/stylesheets/mobile/user.scss
@@ -232,6 +232,10 @@
       margin-bottom: 4px;
       font-size: 14px;
     }
+    .edit-reason {
+      background-color: #ffffcc;
+      padding: 3px 5px 5px 5px;
+    }
   }
 }
 
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 2a0f35291ba..40710fb98c6 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -5,7 +5,7 @@ require_dependency 'distributed_memoizer'
 class PostsController < ApplicationController
 
   # Need to be logged in for all actions here
-  before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :versions, :reply_history]
+  before_filter :ensure_logged_in, except: [:show, :replies, :by_number, :short_link, :reply_history, :revisions]
 
   skip_before_filter :store_incoming_links, only: [:short_link]
   skip_before_filter :check_xhr, only: [:markdown,:short_link]
@@ -65,8 +65,9 @@ class PostsController < ApplicationController
 
     # to stay consistent with the create api,
     #  we should allow for title changes and category changes here
-    # we should also move all of this to a post updater.
+    #  we should also move all of this to a post updater.
     if post.post_number == 1 && (params[:title] || params[:post][:category])
+      post.topic.acting_user = current_user
       post.topic.title = params[:title] if params[:title]
       Topic.transaction do
         post.topic.change_category(params[:post][:category])
@@ -84,7 +85,6 @@ class PostsController < ApplicationController
       TopicLink.extract_from(post)
     end
 
-
     if post.errors.present?
       render_json_error(post)
       return
@@ -104,6 +104,12 @@ class PostsController < ApplicationController
     render_json_dump(result)
   end
 
+  def show
+    @post = find_post_from_params
+    @post.revert_to(params[:version].to_i) if params[:version].present?
+    render_post_json(@post)
+  end
+
   def by_number
     @post = Post.where(topic_id: params[:topic_id], post_number: params[:post_number]).first
     guardian.ensure_can_see!(@post)
@@ -114,16 +120,9 @@ class PostsController < ApplicationController
   def reply_history
     @post = Post.where(id: params[:id]).first
     guardian.ensure_can_see!(@post)
-
     render_serialized(@post.reply_history, PostSerializer)
   end
 
-  def show
-    @post = find_post_from_params
-    @post.revert_to(params[:version].to_i) if params[:version].present?
-    render_post_json(@post)
-  end
-
   def destroy
     post = find_post_from_params
     guardian.ensure_can_delete!(post)
@@ -161,18 +160,18 @@ class PostsController < ApplicationController
     render nothing: true
   end
 
-  # Retrieves a list of versions and who made them for a post
-  def versions
-    post = find_post_from_params
-    render_serialized(post.all_versions, VersionSerializer)
-  end
-
   # Direct replies to this post
   def replies
     post = find_post_from_params
     render_serialized(post.replies, PostSerializer)
   end
 
+  def revisions
+    post_revision = find_post_revision_from_params
+    post_revision_serializer = PostRevisionSerializer.new(post_revision, scope: guardian, root: false)
+    render_json_dump(post_revision_serializer)
+  end
+
   def bookmark
     post = find_post_from_params
     if current_user
@@ -185,19 +184,27 @@ class PostsController < ApplicationController
     render nothing: true
   end
 
-
   protected
 
-    def find_post_from_params
-      finder = Post.where(id: params[:id] || params[:post_id])
+  def find_post_from_params
+    finder = Post.where(id: params[:id] || params[:post_id])
+    # Include deleted posts if the user is staff
+    finder = finder.with_deleted if current_user.try(:staff?)
 
-      # Include deleted posts if the user is staff
-      finder = finder.with_deleted if current_user.try(:staff?)
+    post = finder.first
+    guardian.ensure_can_see!(post)
+    post
+  end
 
-      post = finder.first
-      guardian.ensure_can_see!(post)
-      post
-    end
+  def find_post_revision_from_params
+    post_id = params[:id] || params[:post_id]
+    revision = params[:revision].to_i
+    raise Discourse::InvalidParameters.new(:revision) if revision < 2
+
+    post_revision = PostRevision.where(post_id: post_id, number: revision).first
+    guardian.ensure_can_see!(post_revision)
+    post_revision
+  end
 
   def render_post_json(post)
     post_serializer = PostSerializer.new(post, scope: guardian, root: false)
@@ -207,42 +214,43 @@ class PostsController < ApplicationController
 
   private
 
-    def params_key(params)
-      "post##" << Digest::SHA1.hexdigest(params
-        .to_a
-        .concat([["user", current_user.id]])
-        .sort{|x,y| x[0] <=> y[0]}.join do |x,y|
-          "#{x}:#{y}"
-        end)
+  def params_key(params)
+    "post##" << Digest::SHA1.hexdigest(params
+      .to_a
+      .concat([["user", current_user.id]])
+      .sort{|x,y| x[0] <=> y[0]}.join do |x,y|
+        "#{x}:#{y}"
+      end)
+  end
+
+  def create_params
+    permitted = [
+      :raw,
+      :topic_id,
+      :title,
+      :archetype,
+      :category,
+      :target_usernames,
+      :reply_to_post_number,
+      :auto_close_time,
+      :auto_track
+    ]
+
+    # param munging for WordPress
+    params[:auto_track] = !(params[:auto_track].to_s == "false") if params[:auto_track]
+
+    if api_key_valid?
+      # php seems to be sending this incorrectly, don't fight with it
+      params[:skip_validations] = params[:skip_validations].to_s == "true"
+      permitted << :skip_validations
     end
 
-    def create_params
-      permitted = [
-        :raw,
-        :topic_id,
-        :title,
-        :archetype,
-        :category,
-        :target_usernames,
-        :reply_to_post_number,
-        :auto_close_time,
-        :auto_track
-      ]
-
-      # param munging for WordPress
-      params[:auto_track] = !(params[:auto_track].to_s == "false") if params[:auto_track]
-
-      if api_key_valid?
-        # php seems to be sending this incorrectly, don't fight with it
-        params[:skip_validations] = params[:skip_validations].to_s == "true"
-        permitted << :skip_validations
-      end
-
-      params.require(:raw)
-      params.permit(*permitted).tap do |whitelisted|
-          whitelisted[:image_sizes] = params[:image_sizes]
-          # TODO this does not feel right, we should name what meta_data is allowed
-          whitelisted[:meta_data] = params[:meta_data]
-      end
+    params.require(:raw)
+    params.permit(*permitted).tap do |whitelisted|
+        whitelisted[:image_sizes] = params[:image_sizes]
+        # TODO this does not feel right, we should name what meta_data is allowed
+        whitelisted[:meta_data] = params[:meta_data]
     end
+  end
+
 end
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index 58c32a75db9..b8f5572981d 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -101,13 +101,14 @@ class TopicsController < ApplicationController
     # TODO: we may need smarter rules about converting archetypes
     topic.archetype = "regular" if current_user.admin? && archetype == 'regular'
 
+    topic.acting_user = current_user
+
     success = false
     Topic.transaction do
-      success = topic.save
-      success = topic.change_category(params[:category]) if success
+      success = topic.save && topic.change_category(params[:category])
     end
-    # this is used to return the title to the client as it may have been
-    # changed by "TextCleaner"
+
+    # this is used to return the title to the client as it may have been changed by "TextCleaner"
     success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic)
   end
 
diff --git a/app/models/post.rb b/app/models/post.rb
index 3cd9077020e..6b0692e47c8 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -15,8 +15,6 @@ class Post < ActiveRecord::Base
   include RateLimiter::OnCreateRecord
   include Trashable
 
-  versioned if: :raw_changed?
-
   rate_limit
   rate_limit :limit_posts_per_day
 
@@ -36,6 +34,9 @@ class Post < ActiveRecord::Base
 
   has_many :post_details
 
+  has_many :post_revisions
+  has_many :revisions, foreign_key: :post_id, class_name: 'PostRevision'
+
   validates_with ::Validators::PostValidator
 
   # We can pass several creating options to a post via attributes
@@ -317,12 +318,19 @@ class Post < ActiveRecord::Base
     self.cooked = cook(raw, topic_id: topic_id) unless new_record?
   end
 
+  after_save do
+    save_revision if self.version_changed?
+  end
+
+  after_update do
+    update_revision if self.changed?
+  end
+
   def advance_draft_sequence
     return if topic.blank? # could be deleted
     DraftSequence.next!(last_editor_id, topic.draft_key)
   end
 
-
   # TODO: move to post-analyzer?
   # Determine what posts are quoted by this post
   def extract_quoted_post_numbers
@@ -386,10 +394,17 @@ class Post < ActiveRecord::Base
     Post.where(id: post_ids).includes(:user, :topic).order(:id).to_a
   end
 
+  def revert_to(number)
+    return if number >= version
+    post_revision = PostRevision.where(post_id: id, number: number + 1).first
+    post_revision.modifications.each do |attribute, change|
+      attribute = "version" if attribute == "cached_version"
+      write_attribute(attribute, change[0])
+    end
+  end
+
   private
 
-
-
   def parse_quote_into_arguments(quote)
     return {} unless quote.present?
     args = {}
@@ -412,6 +427,27 @@ class Post < ActiveRecord::Base
       Post.where(id: post.id).update_all ['reply_count = reply_count + 1']
     end
   end
+
+  def save_revision
+    modifications = changes.extract!(:raw, :cooked, :edit_reason)
+    # make sure cooked is always present (oneboxes might not change the cooked post)
+    modifications["cooked"] = [self.cooked, self.cooked] unless modifications["cooked"].present?
+    PostRevision.create!(
+      user_id: last_editor_id,
+      post_id: id,
+      number: version,
+      modifications: modifications
+    )
+  end
+
+  def update_revision
+    revision = PostRevision.where(post_id: id, number: version).first
+    return unless revision
+    revision.user_id = last_editor_id
+    revision.modifications = changes.extract!(:raw, :cooked, :edit_reason)
+    revision.save
+  end
+
 end
 
 # == Schema Information
@@ -427,7 +463,7 @@ end
 #  created_at              :datetime         not null
 #  updated_at              :datetime         not null
 #  reply_to_post_number    :integer
-#  cached_version          :integer          default(1), not null
+#  version                 :integer          default(1), not null
 #  reply_count             :integer          default(0), not null
 #  quote_count             :integer          default(0), not null
 #  deleted_at              :datetime
diff --git a/app/models/post_alert_observer.rb b/app/models/post_alert_observer.rb
index 251d5605b0f..9ceb3f097b0 100644
--- a/app/models/post_alert_observer.rb
+++ b/app/models/post_alert_observer.rb
@@ -1,5 +1,5 @@
 class PostAlertObserver < ActiveRecord::Observer
-  observe :post, VestalVersions::Version, :post_action
+  observe :post, :post_action, :post_revision
 
   # Dispatch to an after_save_#{class_name} method
   def after_save(model)
@@ -46,15 +46,14 @@ class PostAlertObserver < ActiveRecord::Observer
                         post_action_id: post_action.id)
   end
 
-  def after_create_version(version)
-    post = version.versioned
+  def after_create_post_revision(post_revision)
+    post = post_revision.post
 
-    return unless post.is_a?(Post)
-    return if version.user.blank?
-    return if version.user_id == post.user_id
+    return if post_revision.user.blank?
+    return if post_revision.user_id == post.user_id
     return if post.topic.private_message?
 
-    create_notification(post.user, Notification.types[:edited], post, display_username: version.user.username)
+    create_notification(post.user, Notification.types[:edited], post, display_username: post_revision.user.username)
   end
 
   def after_create_post(post)
diff --git a/app/models/post_revision.rb b/app/models/post_revision.rb
new file mode 100644
index 00000000000..280c7aea99c
--- /dev/null
+++ b/app/models/post_revision.rb
@@ -0,0 +1,6 @@
+class PostRevision < ActiveRecord::Base
+  belongs_to :post
+  belongs_to :user
+
+  serialize :modifications, Hash
+end
diff --git a/app/models/topic.rb b/app/models/topic.rb
index f539e2eef0f..20ece18211c 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -26,8 +26,6 @@ class Topic < ActiveRecord::Base
     2**31 - 1
   end
 
-  versioned if: :new_version_required?
-
   def featured_users
     @featured_users ||= TopicFeaturedUsers.new(self)
   end
@@ -97,6 +95,9 @@ class Topic < ActiveRecord::Base
   has_many :topic_invites
   has_many :invites, through: :topic_invites, source: :invite
 
+  has_many :topic_revisions
+  has_many :revisions, foreign_key: :topic_id, class_name: 'TopicRevision'
+
   # When we want to temporarily attach some data to a forum topic (usually before serialization)
   attr_accessor :user_data
   attr_accessor :posters  # TODO: can replace with posters_summary once we remove old list code
@@ -177,6 +178,8 @@ class Topic < ActiveRecord::Base
   end
 
   after_save do
+    save_revision if should_create_new_version?
+
     return if skip_callbacks
 
     if auto_close_at and (auto_close_at_changed? or auto_close_user_id_changed?)
@@ -184,6 +187,19 @@ class Topic < ActiveRecord::Base
     end
   end
 
+  def save_revision
+    TopicRevision.create!(
+      user_id: acting_user.id,
+      topic_id: id,
+      number: TopicRevision.where(topic_id: id).count + 2,
+      modifications: changes.extract!(:category, :title)
+    )
+  end
+
+  def should_create_new_version?
+    !new_record? && (category_id_changed? || title_changed?)
+  end
+
   def self.top_viewed(max = 10)
     Topic.listable_topics.visible.secured.order('views desc').limit(max)
   end
@@ -659,6 +675,14 @@ class Topic < ActiveRecord::Base
     category && category.read_restricted
   end
 
+  def acting_user
+    @acting_user || user
+  end
+
+  def acting_user=(u)
+    @acting_user = u
+  end
+
   private
 
   def update_category_topic_count_by(num)
diff --git a/app/models/topic_revision.rb b/app/models/topic_revision.rb
new file mode 100644
index 00000000000..c8e7c12da22
--- /dev/null
+++ b/app/models/topic_revision.rb
@@ -0,0 +1,6 @@
+class TopicRevision < ActiveRecord::Base
+  belongs_to :topic
+  belongs_to :user
+
+  serialize :modifications, Hash
+end
diff --git a/app/models/user_action.rb b/app/models/user_action.rb
index 0d64a6c8a39..c9417838f1c 100644
--- a/app/models/user_action.rb
+++ b/app/models/user_action.rb
@@ -97,7 +97,8 @@ SELECT
   coalesce(p.cooked, p2.cooked) cooked,
   CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted,
   p.hidden,
-  p.post_type
+  p.post_type,
+  p.edit_reason
 FROM user_actions as a
 JOIN topics t on t.id = a.target_topic_id
 LEFT JOIN posts p on p.id = a.target_post_id
diff --git a/app/serializers/post_revision_serializer.rb b/app/serializers/post_revision_serializer.rb
new file mode 100644
index 00000000000..1f176fddd63
--- /dev/null
+++ b/app/serializers/post_revision_serializer.rb
@@ -0,0 +1,71 @@
+require_dependency "discourse_diff"
+
+class PostRevisionSerializer < ApplicationSerializer
+  attributes :post_id,
+             :version,
+             :revisions_count,
+             :username,
+             :display_username,
+             :avatar_template,
+             :created_at,
+             :edit_reason,
+             :inline,
+             :side_by_side,
+             :side_by_side_markdown
+
+  def version
+    object.number
+  end
+
+  def revisions_count
+    object.post.version
+  end
+
+  def username
+    object.user.username_lower
+  end
+
+  def display_username
+    object.user.username
+  end
+
+  def avatar_template
+    object.user.avatar_template
+  end
+
+  def edit_reason
+    return unless object.modifications["edit_reason"].present?
+    object.modifications["edit_reason"][1]
+  end
+
+  def inline
+    DiscourseDiff.new(previous_cooked, cooked).inline_html
+  end
+
+  def side_by_side
+    DiscourseDiff.new(previous_cooked, cooked).side_by_side_html
+  end
+
+  def side_by_side_markdown
+    DiscourseDiff.new(previous_raw, raw).side_by_side_text
+  end
+
+  private
+
+  def previous_cooked
+    @previous_cooked ||= object.modifications["cooked"][0]
+  end
+
+  def previous_raw
+    @previous_raw ||= object.modifications["raw"][0]
+  end
+
+  def cooked
+    @cooked ||= object.modifications["cooked"][1]
+  end
+
+  def raw
+    @raw ||= object.modifications["raw"][1]
+  end
+
+end
diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb
index 234cff159ae..5f6253afaf9 100644
--- a/app/serializers/post_serializer.rb
+++ b/app/serializers/post_serializer.rb
@@ -98,10 +98,6 @@ class PostSerializer < BasicPostSerializer
     object.score || 0
   end
 
-  def version
-    object.cached_version
-  end
-
   def user_title
     object.user.try(:title)
   end
diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb
index 5f2345f6f38..ffcc6505e6f 100644
--- a/app/serializers/user_action_serializer.rb
+++ b/app/serializers/user_action_serializer.rb
@@ -21,7 +21,8 @@ class UserActionSerializer < ApplicationSerializer
              :title,
              :deleted,
              :hidden,
-             :moderator_action
+             :moderator_action,
+             :edit_reason
 
   def excerpt
     PrettyText.excerpt(object.cooked,300) if object.cooked
diff --git a/app/serializers/version_serializer.rb b/app/serializers/version_serializer.rb
deleted file mode 100644
index a5d4f639712..00000000000
--- a/app/serializers/version_serializer.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-class VersionSerializer < ApplicationSerializer
-
-  attributes :number, :display_username, :created_at
-
-  def number
-    object[:number]
-  end
-
-  def display_username
-    object[:display_username]
-  end
-
-  def created_at
-    object[:created_at]
-  end
-
-end
diff --git a/config/initializers/vestal_versions.rb b/config/initializers/vestal_versions.rb
deleted file mode 100644
index 36700e6dba8..00000000000
--- a/config/initializers/vestal_versions.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-VestalVersions.configure do |config|
-  # Place any global options here. For example, in order to specify your own version model to use
-  # throughout the application, simply specify:
-  #
-  # config.class_name = "MyCustomVersion"
-  #
-  # Any options passed to the "versioned" method in the model itself will override this global
-  # configuration.
-end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 207357c2b27..50131ce9882 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -946,6 +946,26 @@ en:
           one: "Are you sure you want to delete that post?"
           other: "Are you sure you want to delete all those posts?"
 
+      revisions:
+        controls:
+          first: "First revision"
+          previous: "Previous revision"
+          next: "Next revision"
+          last: "Last revision"
+          comparing_previous_to_current_out_of_total: "<strong>#{{previous}}</strong> vs. <strong>#{{current}}</strong> (out of {{total}})"
+        displays:
+          inline:
+            title: "Show the rendered output with additions and removals inline"
+            button: '<i class="fa fa-square-o"></i> HTML'
+          side_by_side:
+            title: "Show the rendered output diffs side-by-side"
+            button: '<i class="fa fa-columns"></i> HTML'
+          side_by_side_markdown:
+            title: "Show the markdown source diffs side-by-side"
+            button: '<i class="fa fa-columns"></i> Markdown'
+        details:
+          edited_by: "Edited by"
+
     category:
       can: 'can&hellip; '
       none: '(no category)'
diff --git a/config/routes.rb b/config/routes.rb
index 3b4ed7d84f0..d4dcb3a2133 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -162,9 +162,9 @@ Discourse::Application.routes.draw do
   get 'posts/by_number/:topic_id/:post_number' => 'posts#by_number'
   get 'posts/:id/reply-history' => 'posts#reply_history'
   resources :posts do
-    get 'versions'
     put 'bookmark'
     get 'replies'
+    get 'revisions/:revision' => 'posts#revisions'
     put 'recover'
     collection do
       delete 'destroy_many'
diff --git a/db/migrate/20131209091702_create_post_revisions.rb b/db/migrate/20131209091702_create_post_revisions.rb
new file mode 100644
index 00000000000..7d12d8cb88c
--- /dev/null
+++ b/db/migrate/20131209091702_create_post_revisions.rb
@@ -0,0 +1,25 @@
+class CreatePostRevisions < ActiveRecord::Migration
+  def up
+    create_table :post_revisions do |t|
+      t.belongs_to :user
+      t.belongs_to :post
+      t.text :modifications
+      t.integer :number
+      t.timestamps
+    end
+
+    execute "INSERT INTO post_revisions (user_id, post_id, modifications, number, created_at, updated_at)
+             SELECT user_id, versioned_id, modifications, number, created_at, updated_at
+             FROM   versions
+             WHERE  versioned_type = 'Post'"
+
+    change_table :post_revisions do |t|
+      t.index :post_id
+      t.index [:post_id, :number]
+    end
+  end
+
+  def down
+    drop_table :post_revisions
+  end
+end
diff --git a/db/migrate/20131209091742_create_topic_revisions.rb b/db/migrate/20131209091742_create_topic_revisions.rb
new file mode 100644
index 00000000000..a6bddd046de
--- /dev/null
+++ b/db/migrate/20131209091742_create_topic_revisions.rb
@@ -0,0 +1,25 @@
+class CreateTopicRevisions < ActiveRecord::Migration
+  def up
+    create_table :topic_revisions do |t|
+      t.belongs_to :user
+      t.belongs_to :topic
+      t.text :modifications
+      t.integer :number
+      t.timestamps
+    end
+
+    execute "INSERT INTO topic_revisions (user_id, topic_id, modifications, number, created_at, updated_at)
+             SELECT user_id, versioned_id, modifications, number, created_at, updated_at
+             FROM   versions
+             WHERE  versioned_type = 'Topic'"
+
+    change_table :topic_revisions do |t|
+      t.index :topic_id
+      t.index [:topic_id, :number]
+    end
+  end
+
+  def down
+    drop_table :topic_revisions
+  end
+end
diff --git a/db/migrate/20131210234530_rename_version_column.rb b/db/migrate/20131210234530_rename_version_column.rb
new file mode 100644
index 00000000000..04f811e7dc7
--- /dev/null
+++ b/db/migrate/20131210234530_rename_version_column.rb
@@ -0,0 +1,9 @@
+class RenameVersionColumn < ActiveRecord::Migration
+
+  def change
+    add_column :posts, :version, :integer, default: 1, null: false
+    execute "UPDATE posts SET version = cached_version"
+    remove_column :posts, :cached_version
+  end
+
+end
diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb
index 9659a83b79f..77239b3fec7 100644
--- a/lib/cooked_post_processor.rb
+++ b/lib/cooked_post_processor.rb
@@ -226,7 +226,7 @@ class CookedPostProcessor
     # have we enough disk space?
     return if disable_if_low_on_disk_space
     # we only want to run the job whenever it's changed by a user
-    return if @post.updated_by == Discourse.system_user
+    return if @post.last_editor_id == Discourse.system_user.id
     # make sure no other job is scheduled
     Jobs.cancel_scheduled_job(:pull_hotlinked_images, post_id: @post.id)
     # schedule the job
diff --git a/lib/diff_engine.rb b/lib/diff_engine.rb
index 4de246c359c..8378335f5d5 100644
--- a/lib/diff_engine.rb
+++ b/lib/diff_engine.rb
@@ -1,6 +1,4 @@
-require 'diffy'
-# This class is used to generate diffs, it will be consumed by the UI on
-# on the client the displays diffs.
+# This class is used to generate diffs, it will be consumed by the UI on the client that displays diffs.
 #
 # There are potential performance issues associated with diffing large amounts of completely
 # different text, see answer here for optimization if needed
@@ -8,18 +6,20 @@ require 'diffy'
 
 class DiffEngine
 
-  # generate an html friendly diff similar to the way Stack Exchange generates
-  # html diffs
+  # Generate an html friendly diff
   #
   #  returns: html containing decorations indicating the changes
   def self.html_diff(html_before, html_after)
-    Diffy::Diff.new(html_before, html_after, {allow_empty_diff: false}).to_s(:html)
+    # tokenize
+    # remove leading/trailing common
+    # SES
+    # format diff
   end
 
-  # same as html diff, except that it operates on markdown
+  # Same as html diff, except that it operates on markdown
   #
   # returns html containing decorated areas where diff happened
   def self.markdown_diff(markdown_before, markdown_after)
-    Diffy::Diff.new(markdown_before, markdown_after).to_s(:html)
+
   end
 end
diff --git a/lib/discourse_diff.rb b/lib/discourse_diff.rb
new file mode 100644
index 00000000000..cf0d35eda66
--- /dev/null
+++ b/lib/discourse_diff.rb
@@ -0,0 +1,265 @@
+require_dependency "onpdiff"
+
+class DiscourseDiff
+
+  MAX_DIFFERENCE = 200
+
+  def initialize(before, after)
+    @before = before
+    @after = after
+
+    @block_by_block_diff = ONPDiff.new(tokenize_html_blocks(@before), tokenize_html_blocks(@after)).diff
+    @line_by_line_diff = ONPDiff.new(tokenize_line(@before), tokenize_line(@after)).short_diff
+  end
+
+  def inline_html
+    i = 0
+    inline = []
+    while i < @block_by_block_diff.length
+      op_code = @block_by_block_diff[i][1]
+      if op_code == :common then inline << @block_by_block_diff[i][0]
+      else
+        if op_code == :delete
+          opposite_op_code = :add
+          klass = "del"
+          first = i
+          second = i + 1
+        else
+          opposite_op_code = :delete
+          klass = "ins"
+          first = i + 1
+          second = i
+        end
+
+        if i + 1 < @block_by_block_diff.length && @block_by_block_diff[i + 1][1] == opposite_op_code
+          diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff
+          inline << generate_inline_html(diff)
+          i += 1
+        else
+          inline << add_class_or_wrap_in_tags(@block_by_block_diff[i][0], klass)
+        end
+      end
+      i += 1
+    end
+
+    "<div class=\"inline-diff\">#{inline.join}</div>"
+  end
+
+  def side_by_side_html
+    i = 0
+    left, right = [], []
+    while i < @block_by_block_diff.length
+      op_code = @block_by_block_diff[i][1]
+      if op_code == :common
+        left << @block_by_block_diff[i][0]
+        right << @block_by_block_diff[i][0]
+      else
+        if op_code == :delete
+          opposite_op_code = :add
+          side = left
+          klass = "del"
+          first = i
+          second = i + 1
+        else
+          opposite_op_code = :delete
+          side = right
+          klass = "ins"
+          first = i + 1
+          second = i
+        end
+
+        if i + 1 < @block_by_block_diff.length && @block_by_block_diff[i + 1][1] == opposite_op_code
+          diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff
+          deleted, inserted = generate_side_by_side_html(diff)
+          left << deleted
+          right << inserted
+          i += 1
+        else
+          side << add_class_or_wrap_in_tags(@block_by_block_diff[i][0], klass)
+        end
+      end
+      i += 1
+    end
+
+    "<div class=\"span8\">#{left.join}</div><div class=\"span8 offset1\">#{right.join}</div>"
+  end
+
+  def side_by_side_text
+    i = 0
+    table = ["<table class=\"markdown\">"]
+    while i < @line_by_line_diff.length
+      table << "<tr>"
+      op_code = @line_by_line_diff[i][1]
+      if op_code == :common
+        table << "<td>#{CGI::escapeHTML(@line_by_line_diff[i][0])}</td>"
+        table << "<td>#{CGI::escapeHTML(@line_by_line_diff[i][0])}</td>"
+      else
+        if op_code == :delete
+          opposite_op_code = :add
+          first = i
+          second = i + 1
+        else
+          opposite_op_code = :delete
+          first = i + 1
+          second = i
+        end
+
+        if i + 1 < @line_by_line_diff.length && @line_by_line_diff[i + 1][1] == opposite_op_code
+          before_tokens, after_tokens = tokenize_text(@line_by_line_diff[first][0]), tokenize_text(@line_by_line_diff[second][0])
+          if (before_tokens.length - after_tokens.length).abs > MAX_DIFFERENCE
+            before_tokens, after_tokens = tokenize_line(@line_by_line_diff[first][0]), tokenize_line(@line_by_line_diff[second][0])
+          end
+          diff = ONPDiff.new(before_tokens, after_tokens).short_diff
+          deleted, inserted = generate_side_by_side_text(diff)
+          table << "<td class=\"diff-del\">#{deleted.join}</td>"
+          table << "<td class=\"diff-ins\">#{inserted.join}</td>"
+          i += 1
+        else
+          if op_code == :delete
+            table << "<td class=\"diff-del\">#{CGI::escapeHTML(@line_by_line_diff[i][0])}</td>"
+            table << "<td></td>"
+          else
+            table << "<td></td>"
+            table << "<td class=\"diff-ins\">#{CGI::escapeHTML(@line_by_line_diff[i][0])}</td>"
+          end
+        end
+      end
+      table << "</tr>"
+      i += 1
+    end
+    table << "</table>"
+
+    table.join
+  end
+
+  private
+
+  def tokenize_line(text)
+    text.scan(/[^\r\n]+[\r\n]*/)
+  end
+
+  def tokenize_text(text)
+    t, tokens = [], []
+    i = 0
+    while i < text.length
+      if text[i] =~ /\w/
+        t << text[i]
+      elsif text[i] =~ /[ \t]/ && t.join =~ /^\w+$/
+        begin
+          t << text[i]
+          i += 1
+        end while i < text.length && text[i] =~ /[ \t]/
+        i -= 1
+        tokens << t.join
+        t = []
+      else
+        tokens << t.join if t.length > 0
+        tokens << text[i]
+        t = []
+      end
+      i += 1
+    end
+    tokens << t.join if t.length > 0
+    tokens
+  end
+
+  def tokenize_html_blocks(html)
+    Nokogiri::HTML.fragment(html).search("./*").map(&:to_html)
+  end
+
+  def tokenize_html(html)
+    HtmlTokenizer.tokenize(html)
+  end
+
+  def add_class_or_wrap_in_tags(html_or_text, klass)
+    index_of_next_chevron = html_or_text.index(">")
+    if html_or_text.length > 0 && html_or_text[0] == '<' && index_of_next_chevron
+      index_of_class = html_or_text.index("class=")
+      if index_of_class.nil? || index_of_class > index_of_next_chevron
+        # we do not have a class for the current tag
+        # add it right before the ">"
+        html_or_text.insert(index_of_next_chevron, " class=\"diff-#{klass}\"")
+      else
+        # we have a class, insert it at the beginning
+        html_or_text.insert(index_of_class + "class=".length + 1, "diff-#{klass} ")
+      end
+    else
+      "<#{klass}>#{html_or_text}</#{klass}>"
+    end
+  end
+
+  def generate_inline_html(diff)
+    inline = []
+    diff.each do |d|
+      case d[1]
+      when :common then inline << d[0]
+      when :delete then inline << add_class_or_wrap_in_tags(d[0], "del")
+      when :add then inline << add_class_or_wrap_in_tags(d[0], "ins")
+      end
+    end
+    inline
+  end
+
+  def generate_side_by_side_html(diff)
+    deleted, inserted = [], []
+    diff.each do |d|
+      case d[1]
+      when :common
+        deleted << d[0]
+        inserted << d[0]
+      when :delete then deleted << add_class_or_wrap_in_tags(d[0], "del")
+      when :add then inserted << add_class_or_wrap_in_tags(d[0], "ins")
+      end
+    end
+    [deleted, inserted]
+  end
+
+  def generate_side_by_side_text(diff)
+    deleted, inserted = [], []
+    diff.each do |d|
+      case d[1]
+      when :common
+        deleted << d[0]
+        inserted << d[0]
+      when :delete then deleted << "<del>#{CGI::escapeHTML(d[0])}</del>"
+      when :add then inserted << "<ins>#{CGI::escapeHTML(d[0])}</ins>"
+      end
+    end
+    [deleted, inserted]
+  end
+
+  class HtmlTokenizer < Nokogiri::XML::SAX::Document
+
+    attr_accessor :tokens
+
+    def initialize
+      @tokens = []
+    end
+
+    def self.tokenize(html)
+      me = new
+      parser = Nokogiri::HTML::SAX::Parser.new(me)
+      parser.parse("<html><body>#{html}</body></html>")
+      me.tokens
+    end
+
+    USELESS_TAGS = %w{html body}
+    def start_element(name, attributes = [])
+      return if USELESS_TAGS.include?(name)
+      attrs = attributes.map { |a| " #{a[0]}=\"#{a[1]}\"" }.join
+      @tokens << "<#{name}#{attrs}>"
+    end
+
+    AUTOCLOSING_TAGS = %w{area base br col embed hr img input meta}
+    def end_element(name)
+      return if USELESS_TAGS.include?(name) || AUTOCLOSING_TAGS.include?(name)
+      @tokens << "</#{name}>"
+    end
+
+    def characters(string)
+      @tokens.concat string.scan(/(\W|\w+[ \t]*)/).flatten
+    end
+
+  end
+
+end
diff --git a/lib/guardian.rb b/lib/guardian.rb
index 0d1806ebe45..e261d492ace 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -376,6 +376,10 @@ class Guardian
     post.present? && (is_staff? || (!post.deleted_at.present? && can_see_topic?(post.topic)))
   end
 
+  def can_see_post_revision?(post_revision)
+    post_revision.present? && (is_staff? || can_see_post?(post_revision.post))
+  end
+
   def can_see_category?(category)
     not(category.read_restricted) || secure_category_ids.include?(category.id)
   end
diff --git a/lib/onpdiff.rb b/lib/onpdiff.rb
new file mode 100644
index 00000000000..04fd812ea50
--- /dev/null
+++ b/lib/onpdiff.rb
@@ -0,0 +1,153 @@
+# Use "An O(NP) Sequence Comparison Algorithm" as described by Sun Wu, Udi Manber and Gene Myers
+# in http://www.itu.dk/stud/speciale/bepjea/xwebtex/litt/an-onp-sequence-comparison-algorithm.pdf
+class ONPDiff
+
+  def initialize(a, b)
+    @a, @b = a, b
+    @m, @n = a.length, b.length
+    @backtrack = []
+    if @reverse = @m > @n
+      @a, @b = @b, @a
+      @m, @n = @n, @m
+    end
+    @offset = @m + 1
+    @delta = @n - @m
+  end
+
+  def diff
+    @diff ||= build_diff_script(compose)
+  end
+
+  def short_diff
+    @short_diff ||= build_short_diff_script(compose)
+  end
+
+  private
+
+  def compose
+    return @shortest_path if @shortest_path
+
+    size = @m + @n + 3
+    fp = Array.new(size) { |i| -1 }
+    @path = Array.new(size) { |i| -1 }
+    p = -1
+
+    begin
+      p += 1
+
+      k = -p
+      while k <= @delta - 1
+        fp[k + @offset] = snake(k, fp[k - 1 + @offset] + 1, fp[k + 1 + @offset])
+        k += 1
+      end
+
+      k = @delta + p
+      while k >= @delta + 1
+        fp[k + @offset] = snake(k, fp[k - 1 + @offset] + 1, fp[k + 1 + @offset])
+        k -= 1
+      end
+
+      fp[@delta + @offset] = snake(@delta, fp[@delta - 1 + @offset] + 1, fp[@delta + 1 + @offset])
+
+    end until fp[@delta + @offset] == @n
+
+    r = @path[@delta + @offset]
+
+    @shortest_path = []
+    while r != -1
+      @shortest_path << [@backtrack[r][0], @backtrack[r][1]]
+      r = @backtrack[r][2]
+    end
+
+    @shortest_path
+  end
+
+  def snake(k, p, pp)
+    r = p > pp ? @path[k - 1 + @offset] : @path[k + 1 + @offset]
+    y = [p, pp].max
+    x = y - k
+
+    while x < @m && y < @n && @a[x] == @b[y]
+      x += 1
+      y += 1
+    end
+
+    @path[k + @offset] = @backtrack.length
+    @backtrack << [x, y, r]
+
+    y
+  end
+
+  def build_diff_script(shortest_path)
+    ses = []
+    x, y = 1, 1
+    px, py = 0, 0
+    i = shortest_path.length - 1
+    while i >= 0
+      while px < shortest_path[i][0] || py < shortest_path[i][1]
+        if shortest_path[i][1] - shortest_path[i][0] > py - px
+          t = @reverse ? :delete : :add
+          ses << [@b[py], t]
+          y += 1
+          py += 1
+        elsif shortest_path[i][1] - shortest_path[i][0] < py - px
+          t = @reverse ? :add : :delete
+          ses << [@a[px], t]
+          x += 1
+          px += 1
+        else
+          ses << [@a[px], :common]
+          x += 1
+          y += 1
+          px += 1
+          py += 1
+        end
+      end
+      i -= 1
+    end
+    ses
+  end
+
+  def build_short_diff_script(shortest_path)
+    ses = []
+    x, y = 1, 1
+    px, py = 0, 0
+    i = shortest_path.length - 1
+    while i >= 0
+      while px < shortest_path[i][0] || py < shortest_path[i][1]
+        if shortest_path[i][1] - shortest_path[i][0] > py - px
+          t = @reverse ? :delete : :add
+          if ses.length > 0 && ses[-1][1] == t
+            ses[-1][0] << @b[py]
+          else
+            ses << [@b[py], t]
+          end
+          y += 1
+          py += 1
+        elsif shortest_path[i][1] - shortest_path[i][0] < py - px
+          t = @reverse ? :add : :delete
+          if ses.length > 0 && ses[-1][1] == t
+            ses[-1][0] << @a[px]
+          else
+            ses << [@a[px], t]
+          end
+          x += 1
+          px += 1
+        else
+          if ses.length > 0 && ses[-1][1] == :common
+            ses[-1][0] << @a[px]
+          else
+            ses << [@a[px], :common]
+          end
+          x += 1
+          y += 1
+          px += 1
+          py += 1
+        end
+      end
+      i -= 1
+    end
+    ses
+  end
+
+end
diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb
index 06bc3383206..4ebe665a0fb 100644
--- a/lib/post_destroyer.rb
+++ b/lib/post_destroyer.rb
@@ -124,7 +124,7 @@ class PostDestroyer
     Post.transaction do
       @post.update_column(:user_deleted, false)
       @post.skip_unique_check = true
-      @post.revise(@user, @post.versions.last.modifications["raw"][0], force_new_version: true)
+      @post.revise(@user, @post.revisions.last.modifications["raw"][0], force_new_version: true)
       @post.update_flagged_posts_count
     end
   end
diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb
index 1395cc70ae3..e118d5af522 100644
--- a/lib/post_revisor.rb
+++ b/lib/post_revisor.rb
@@ -1,4 +1,5 @@
 require 'edit_rate_limiter'
+
 class PostRevisor
 
   attr_reader :category_changed
@@ -12,12 +13,12 @@ class PostRevisor
     return false if not should_revise?
 
     @post.acting_user = @user
-    @post.updated_by = @user
     revise_post
     update_category_description
     post_process_post
     update_topic_word_counts
     @post.advance_draft_sequence
+
     true
   end
 
@@ -31,7 +32,7 @@ class PostRevisor
     if should_create_new_version?
       revise_and_create_new_version
     else
-      revise_without_creating_a_new_version
+      update_post
     end
   end
 
@@ -47,7 +48,7 @@ class PostRevisor
 
   def revise_and_create_new_version
     Post.transaction do
-      @post.cached_version = @post.version + 1
+      @post.version += 1
       @post.last_version_at = get_revised_at
       update_post
       EditRateLimiter.new(@post.user).performed! unless @opts[:bypass_rate_limiter] == true
@@ -55,12 +56,6 @@ class PostRevisor
     end
   end
 
-  def revise_without_creating_a_new_version
-    @post.skip_version do
-      update_post
-    end
-  end
-
   def bump_topic
     unless Post.where('post_number > ? and topic_id = ?', @post.post_number, @post.topic_id).exists?
       @post.topic.update_column(:bumped_at, Time.now)
@@ -76,7 +71,6 @@ class PostRevisor
   def update_post
     @post.raw = @new_raw
     @post.word_count = @new_raw.scan(/\w+/).size
-    @post.updated_by = @user
     @post.last_editor_id = @user.id
     @post.edit_reason = @opts[:edit_reason] if @opts[:edit_reason]
 
diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb
index a84a9d2965f..ec674739b56 100644
--- a/spec/components/cooked_post_processor_spec.rb
+++ b/spec/components/cooked_post_processor_spec.rb
@@ -296,7 +296,7 @@ describe CookedPostProcessor do
       before { SiteSetting.stubs(:download_remote_images_to_local).returns(true) }
 
       it "runs only when a user updated the post" do
-        post.updated_by = Discourse.system_user
+        post.last_editor_id = Discourse.system_user.id
         Jobs.expects(:cancel_scheduled_job).never
         cpp.pull_hotlinked_images
       end
diff --git a/spec/components/diff_engine_spec.rb b/spec/components/diff_engine_spec.rb
deleted file mode 100644
index 64e9351a266..00000000000
--- a/spec/components/diff_engine_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-require 'spec_helper'
-require 'diff_engine'
-
-describe DiffEngine do
-
-  let(:html_before) do
-    <<-HTML.strip_heredoc
-    <context>
-    <original>text</original>
-    </context>
-    HTML
-  end
-
-  let(:markdown_special_characters) do
-    "=\`*_{}[]()#+-.!"
-  end
-
-  it "escapes input html to markup with diff html" do
-    diff = DiffEngine.html_diff("<html>", "")
-
-    diff.should include("&lt;html&gt;")
-  end
-
-  it "generates an html diff with ins and dels for changed" do
-    html_after = html_before
-      .gsub(/original/, "changed")
-
-    diff = DiffEngine.html_diff(html_before, html_after)
-
-    diff.should match(/del.*?original.*?del/)
-    diff.should match(/ins.*?changed.*?ins/)
-  end
-
-  it "generates an html diff with only ins for inserted" do
-    html_after = "#{html_before}\nnew"
-
-    diff = DiffEngine.html_diff(html_before, html_after)
-
-    diff.should include("ins")
-    diff.should_not include("del")
-  end
-
-  it "generates an html diff with only unchanged for unchanged" do
-    html_after = html_before
-
-    diff = DiffEngine.html_diff(html_before, html_after)
-
-    diff.should include("unchanged")
-    diff.should_not include("del", "ins")
-  end
-
-  it "handles markdown special characters" do
-    diff = DiffEngine.markdown_diff(markdown_special_characters, "")
-
-    diff.should include(markdown_special_characters)
-  end
-
-end
diff --git a/spec/components/post_revisor_spec.rb b/spec/components/post_revisor_spec.rb
index c20a55f9ba6..b615ce3adeb 100644
--- a/spec/components/post_revisor_spec.rb
+++ b/spec/components/post_revisor_spec.rb
@@ -19,8 +19,8 @@ describe PostRevisor do
         subject.revise!(post.user, post.raw).should be_false
       end
 
-      it "doesn't change cached_version" do
-        lambda { subject.revise!(post.user, post.raw); post.reload }.should_not change(post, :cached_version)
+      it "doesn't change version" do
+        lambda { subject.revise!(post.user, post.raw); post.reload }.should_not change(post, :version)
       end
 
     end
@@ -32,12 +32,12 @@ describe PostRevisor do
         post.reload
       end
 
-      it 'does not update cached_version' do
-        post.cached_version.should == 1
+      it 'does not update version' do
+        post.version.should == 1
       end
 
-      it 'does not create a new version' do
-        post.all_versions.size.should == 1
+      it 'does not create a new revision' do
+        post.revisions.size.should == 0
       end
 
       it "doesn't change the last_version_at" do
@@ -64,12 +64,12 @@ describe PostRevisor do
         subject.category_changed.should be_blank
       end
 
-      it 'updates the cached_version' do
-        post.cached_version.should == 2
+      it 'updates the version' do
+        post.version.should == 2
       end
 
       it 'creates a new version' do
-        post.all_versions.size.should == 2
+        post.revisions.size.should == 1
       end
 
       it "updates the last_version_at" do
@@ -84,7 +84,7 @@ describe PostRevisor do
         end
 
         it "doesn't create a new version if you do another" do
-          post.cached_version.should == 2
+          post.version.should == 2
         end
 
         it "doesn't change last_version_at" do
@@ -105,7 +105,7 @@ describe PostRevisor do
           end
 
           it "does create a new version after the edit window" do
-            post.cached_version.should == 3
+            post.version.should == 3
           end
 
           it "does create a new version after the edit window" do
@@ -199,7 +199,7 @@ describe PostRevisor do
       end
 
       it "marks the admin as the last updater" do
-        post.updated_by.should == changed_by
+        post.last_editor_id.should == changed_by.id
       end
 
     end
@@ -236,20 +236,16 @@ describe PostRevisor do
         post.invalidate_oneboxes.should == true
       end
 
-      it 'increased the cached_version' do
-        post.cached_version.should == 2
+      it 'increased the version' do
+        post.version.should == 2
       end
 
-      it 'has the new version in all_versions' do
-        post.all_versions.size.should == 2
+      it 'has the new revision' do
+        post.revisions.size.should == 1
       end
 
-      it 'has versions' do
-        post.versions.should be_present
-      end
-
-      it "saved the user who made the change in the version" do
-        post.versions.first.user.should be_present
+      it "saved the user who made the change in the revisions" do
+        post.revisions.first.user_id.should == changed_by.id
       end
 
       it "updates the word count" do
@@ -266,11 +262,11 @@ describe PostRevisor do
         end
 
         it 'is a ninja edit, because the second poster posted again quickly' do
-          post.cached_version.should == 2
+          post.version.should == 2
         end
 
         it 'is a ninja edit, because the second poster posted again quickly' do
-          post.all_versions.size.should == 2
+          post.revisions.size.should == 1
         end
       end
     end
diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb
index ef1c495e608..5731ec1755f 100644
--- a/spec/controllers/posts_controller_spec.rb
+++ b/spec/controllers/posts_controller_spec.rb
@@ -70,34 +70,6 @@ describe PostsController do
     end
   end
 
-
-  describe 'versions' do
-
-    shared_examples 'posts_controller versions examples' do
-      it "raises an error if the user doesn't have permission to see the post" do
-        Guardian.any_instance.expects(:can_see?).with(post).returns(false)
-        xhr :get, :versions, post_id: post.id
-        response.should be_forbidden
-      end
-
-      it 'renders JSON' do
-        xhr :get, :versions, post_id: post.id
-        ::JSON.parse(response.body).should be_present
-      end
-    end
-
-    context 'when not logged in' do
-      let(:post) { Fabricate(:post) }
-      include_examples 'posts_controller versions examples'
-    end
-
-    context 'when logged in' do
-      let(:post) { Fabricate(:post, user: log_in) }
-      include_examples 'posts_controller versions examples'
-    end
-
-  end
-
   describe 'delete a post' do
     it 'raises an exception when not logged in' do
       lambda { xhr :delete, :destroy, id: 123 }.should raise_error(Discourse::NotLoggedIn)
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index c98b1b91482..c7e259d0fa6 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -28,15 +28,16 @@ describe Post do
 
   it { should have_many :post_details }
 
+  it { should have_many :post_revisions }
+  it { should have_many :revisions}
+
   it { should rate_limit }
 
   let(:topic) { Fabricate(:topic) }
   let(:post_args) do
-    {user: topic.user, topic: topic}
+    { user: topic.user, topic: topic }
   end
 
-  it_behaves_like "a versioned model"
-
   describe 'scopes' do
 
     describe '#by_newest' do
@@ -57,7 +58,7 @@ describe Post do
 
   end
 
-  describe "versions and deleting/recovery" do
+  describe "revisions and deleting/recovery" do
 
     context 'a post without links' do
       let(:post) { Fabricate(:post, post_args) }
@@ -67,8 +68,8 @@ describe Post do
         post.reload
       end
 
-      it "doesn't create a new version when deleted" do
-        post.versions.count.should == 0
+      it "doesn't create a new revision when deleted" do
+        post.revisions.count.should == 0
       end
 
       describe "recovery" do
@@ -77,8 +78,8 @@ describe Post do
           post.reload
         end
 
-        it "doesn't create a new version when recovered" do
-          post.versions.count.should == 0
+        it "doesn't create a new revision when recovered" do
+          post.revisions.count.should == 0
         end
       end
     end
@@ -481,16 +482,16 @@ describe Post do
     let(:post) { Fabricate(:post, post_args) }
     let(:first_version_at) { post.last_version_at }
 
-    it 'has one version in all_versions' do
-      post.all_versions.size.should == 1
+    it 'has no revision' do
+      post.revisions.size.should == 0
       first_version_at.should be_present
       post.revise(post.user, post.raw).should be_false
     end
 
-
     describe 'with the same body' do
-      it "doesn't change cached_version" do
-        lambda { post.revise(post.user, post.raw); post.reload }.should_not change(post, :cached_version)
+
+      it "doesn't change version" do
+        lambda { post.revise(post.user, post.raw); post.reload }.should_not change(post, :version)
       end
 
     end
@@ -503,8 +504,8 @@ describe Post do
       end
 
       it 'causes no update' do
-        post.cached_version.should == 1
-        post.all_versions.size.should == 1
+        post.version.should == 1
+        post.revisions.size.should == 0
         post.last_version_at.should == first_version_at
       end
 
@@ -520,9 +521,9 @@ describe Post do
         post.reload
       end
 
-      it 'updates the cached_version' do
-        post.cached_version.should == 2
-        post.all_versions.size.should == 2
+      it 'updates the version' do
+        post.version.should == 2
+        post.revisions.size.should == 1
         post.last_version_at.to_i.should == revised_at.to_i
       end
 
@@ -534,7 +535,7 @@ describe Post do
         end
 
         it "doesn't create a new version if you do another" do
-          post.cached_version.should == 2
+          post.version.should == 2
         end
 
         it "doesn't change last_version_at" do
@@ -551,7 +552,7 @@ describe Post do
           end
 
           it "does create a new version after the edit window" do
-            post.cached_version.should == 3
+            post.version.should == 3
           end
 
           it "does create a new version after the edit window" do
@@ -582,10 +583,9 @@ describe Post do
         result.should be_true
         post.raw.should == 'updated body'
         post.invalidate_oneboxes.should == true
-        post.cached_version.should == 2
-        post.all_versions.size.should == 2
-        post.versions.should be_present
-        post.versions.first.user.should be_present
+        post.version.should == 2
+        post.revisions.size.should == 1
+        post.revisions.first.user.should be_present
       end
 
       context 'second poster posts again quickly' do
@@ -596,8 +596,8 @@ describe Post do
         end
 
         it 'is a ninja edit, because the second poster posted again quickly' do
-          post.cached_version.should == 2
-          post.all_versions.size.should == 2
+          post.version.should == 2
+          post.revisions.size.should == 1
         end
 
       end
@@ -615,7 +615,7 @@ describe Post do
       post.post_number.should be_present
       post.excerpt.should be_present
       post.post_type.should == Post.types[:regular]
-      post.versions.should be_blank
+      post.revisions.should be_blank
       post.cooked.should be_present
       post.external_id.should be_present
       post.quote_count.should == 0
diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb
index ab4e97f1d4c..00da3e8afc3 100644
--- a/spec/models/topic_spec.rb
+++ b/spec/models/topic_spec.rb
@@ -21,11 +21,11 @@ describe Topic do
   it { should have_many :topic_allowed_users }
   it { should have_many :allowed_users }
   it { should have_many :invites }
+  it { should have_many :topic_revisions }
+  it { should have_many :revisions }
 
   it { should rate_limit }
 
-  it_behaves_like "a versioned model"
-
   context 'slug' do
 
     let(:title) { "hello world topic" }
@@ -734,22 +734,24 @@ describe Topic do
     end
   end
 
-  describe 'versions' do
+  describe 'revisions' do
     let(:topic) { Fabricate(:topic) }
 
-    it "has version 1 by default" do
-      topic.version.should == 1
+    it "has no revisions by default" do
+      topic.revisions.size.should == 1
     end
 
     context 'changing title' do
+
       before do
         topic.title = "new title for the topic"
         topic.save
       end
 
-      it "creates a new version" do
-        topic.version.should == 2
+      it "creates a new revision" do
+        topic.revisions.size.should == 2
       end
+
     end
 
     context 'changing category' do
@@ -759,8 +761,8 @@ describe Topic do
         topic.change_category(category.name)
       end
 
-      it "creates a new version" do
-        topic.version.should == 2
+      it "creates a new revision" do
+        topic.revisions.size.should == 2
       end
 
       context "removing a category" do
@@ -768,8 +770,8 @@ describe Topic do
           topic.change_category(nil)
         end
 
-        it "creates a new version" do
-          topic.version.should == 3
+        it "creates a new revision" do
+          topic.revisions.size.should == 3
         end
       end
 
@@ -781,8 +783,8 @@ describe Topic do
         topic.save
       end
 
-      it "doesn't craete a new version" do
-        topic.version.should == 1
+      it "doesn't create a new version" do
+        topic.revisions.size.should == 1
       end
     end
 
diff --git a/vendor/assets/javascripts/bootstrap-modal.js b/vendor/assets/javascripts/bootstrap-modal.js
index c831de6b64b..0465cb52568 100644
--- a/vendor/assets/javascripts/bootstrap-modal.js
+++ b/vendor/assets/javascripts/bootstrap-modal.js
@@ -215,4 +215,4 @@
     })
   })
 
-}(window.jQuery);
\ No newline at end of file
+}(window.jQuery);