diff --git a/app/models/post.rb b/app/models/post.rb
index 2e71eb425..066cac96b 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -1,6 +1,7 @@
 require_dependency 'jobs'
 require_dependency 'pretty_text'
 require_dependency 'rate_limiter'
+require_dependency 'post_revisor'
 
 require 'archetype'
 require 'hpricot'
@@ -14,12 +15,6 @@ class Post < ActiveRecord::Base
     FLAG_THRESHOLD_REACHED_AGAIN = 2
   end
 
-  # A custom rate limiter for edits
-  class EditRateLimiter < RateLimiter
-    def initialize(user)
-      super(user, "edit-post:#{Date.today.to_s}", SiteSetting.max_edits_per_day, 1.day.to_i)
-    end
-  end
 
   versioned
 
@@ -115,6 +110,11 @@ class Post < ActiveRecord::Base
     @cooked_document ||= Nokogiri::HTML.fragment(self.cooked)
   end
 
+  def reset_cooked
+    @cooked_document = nil
+    self.cooked = nil
+  end
+
   def image_count
     return 0 unless self.raw.present?
     cooked_document.search("img.emoji").remove
@@ -290,77 +290,14 @@ class Post < ActiveRecord::Base
     self.save
   end
 
-  # Update the body of a post. Will create a new version when appropriate
-  def revise(updated_by, new_raw, opts={})
-
-    # Only update if it changes
-    return false if self.raw == new_raw
-
-    updater = lambda do |new_version=false|
-
-      # Raw is about to change, enable validations
-      @cooked_document = nil
-      self.cooked = nil
-
-      self.raw = new_raw
-      self.updated_by = updated_by
-      self.last_editor_id = updated_by.id
-
-      if self.hidden && self.hidden_reason_id == HiddenReason::FLAG_THRESHOLD_REACHED
-        self.hidden = false
-        self.hidden_reason_id = nil
-        self.topic.update_attributes(visible: true)
-
-        PostAction.clear_flags!(self, -1)
-      end
-
-      self.save
-    end
-
-    # We can optionally specify when this version was revised. Defaults to now.
-    revised_at = opts[:revised_at] || Time.now
-    new_version = false
-
-    # We always create a new version if the poster has changed
-    new_version = true if (self.last_editor_id != updated_by.id)
-
-    # We always create a new version if it's been greater than the ninja edit window
-    new_version = true if (revised_at - last_version_at) > SiteSetting.ninja_edit_window.to_i
-
-    new_version = true if opts[:force_new_version]
-
-    # Create the new version (or don't)
-    if new_version
-
-      self.cached_version = version + 1
-
-      Post.transaction do
-        self.last_version_at = revised_at
-        updater.call(true)
-        EditRateLimiter.new(updated_by).performed! unless opts[:bypass_rate_limiter]
-
-        # If a new version is created of the last post, bump it.
-        unless Post.where('post_number > ? and topic_id = ?', self.post_number, self.topic_id).exists?
-          topic.update_column(:bumped_at, Time.now) unless opts[:bypass_bump]
-        end
-      end
-
-    else
-      skip_version(&updater)
-    end
-
-    # Invalidate any oneboxes
-    self.invalidate_oneboxes = true
-    trigger_post_process
-
-    true
-  end
-
-
   def url
     "/t/#{Slug.for(topic.title)}/#{topic.id}/#{post_number}"
   end
 
+  def revise(updated_by, new_raw, opts={})
+    PostRevisor.new(self).revise!(updated_by, new_raw, opts)
+  end
+
   # Various callbacks
   before_create do
     self.post_number ||= Topic.next_post_number(topic_id, reply_to_post_number.present?)
diff --git a/lib/edit_rate_limiter.rb b/lib/edit_rate_limiter.rb
new file mode 100644
index 000000000..f91291eda
--- /dev/null
+++ b/lib/edit_rate_limiter.rb
@@ -0,0 +1,6 @@
+require 'rate_limiter'
+class EditRateLimiter < RateLimiter
+  def initialize(user)
+    super(user, "edit-post:#{Date.today.to_s}", SiteSetting.max_edits_per_day, 1.day.to_i)
+  end
+end
diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb
new file mode 100644
index 000000000..c8ec6449e
--- /dev/null
+++ b/lib/post_revisor.rb
@@ -0,0 +1,83 @@
+require 'edit_rate_limiter'
+class PostRevisor
+  def initialize(post)
+    @post = post
+  end
+
+  def revise!(user, new_raw, opts = {})
+    @user, @new_raw, @opts = user, new_raw, opts
+    return false if not should_revise?
+    revise_post
+    post_process_post
+    true
+  end
+
+  private
+
+  def should_revise?
+    @post.raw != @new_raw
+  end
+
+  def revise_post
+    if should_create_new_version?
+      revise_and_create_new_version
+    else
+      revise_without_creating_a_new_version
+    end
+  end
+
+  def get_revised_at
+    @opts[:revised_at] || Time.now
+  end
+
+  def should_create_new_version?
+    (@post.last_editor_id != @user.id) or
+      ((get_revised_at - @post.last_version_at) > SiteSetting.ninja_edit_window.to_i) or
+      @opts[:force_new_version] == true
+  end
+
+  def revise_and_create_new_version
+    Post.transaction do
+      @post.cached_version = @post.version + 1
+      @post.last_version_at = get_revised_at
+      update_post
+      EditRateLimiter.new(@post.user).performed! unless @opts[:bypass_rate_limiter] == true
+      bump_topic unless @opts[:bypass_bump]
+    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)
+    end
+  end
+
+  def update_post
+    @post.reset_cooked
+
+    @post.raw = @new_raw
+    @post.updated_by = @user
+    @post.last_editor_id = @user.id
+
+    if @post.hidden && @post.hidden_reason_id == Post::HiddenReason::FLAG_THRESHOLD_REACHED
+      @post.hidden = false
+      @post.hidden_reason_id = nil
+      @post.topic.update_attributes(visible: true)
+
+      PostAction.clear_flags!(@post, -1)
+    end
+
+    @post.save
+  end
+
+  def post_process_post
+    @post.invalidate_oneboxes = true
+    @post.trigger_post_process
+  end
+end
diff --git a/spec/components/post_revisor_spec.rb b/spec/components/post_revisor_spec.rb
new file mode 100644
index 000000000..386211f47
--- /dev/null
+++ b/spec/components/post_revisor_spec.rb
@@ -0,0 +1,163 @@
+require 'spec_helper'
+require 'post_revisor'
+
+describe PostRevisor do
+
+  let(:topic) { Fabricate(:topic) }
+  let(:post_args) { {user: topic.user, topic: topic} }
+
+  context 'revise' do
+
+    let(:post) { Fabricate(:post, post_args) }
+    let(:first_version_at) { post.last_version_at }
+
+    subject { described_class.new(post) }
+
+    describe 'with the same body' do
+
+      it 'returns false' 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)
+      end
+
+    end
+
+    describe 'ninja editing' do
+      before do
+        SiteSetting.expects(:ninja_edit_window).returns(1.minute.to_i)
+        subject.revise!(post.user, 'updated body', revised_at: post.updated_at + 10.seconds)
+        post.reload
+      end
+
+      it 'does not update cached_version' do
+        post.cached_version.should == 1
+      end
+
+      it 'does not create a new version' do
+        post.all_versions.size.should == 1
+      end
+
+      it "doesn't change the last_version_at" do
+        post.last_version_at.should == first_version_at
+      end
+    end
+
+    describe 'revision much later' do
+
+      let!(:revised_at) { post.updated_at + 2.minutes }
+
+      before do
+        SiteSetting.stubs(:ninja_edit_window).returns(1.minute.to_i)
+        subject.revise!(post.user, 'updated body', revised_at: revised_at)
+        post.reload
+      end
+
+      it 'updates the cached_version' do
+        post.cached_version.should == 2
+      end
+
+      it 'creates a new version' do
+        post.all_versions.size.should == 2
+      end
+
+      it "updates the last_version_at" do
+        post.last_version_at.to_i.should == revised_at.to_i
+      end
+
+      describe "new edit window" do
+
+        before do
+          subject.revise!(post.user, 'yet another updated body', revised_at: revised_at)
+          post.reload
+        end
+
+        it "doesn't create a new version if you do another" do
+          post.cached_version.should == 2
+        end
+
+        it "doesn't change last_version_at" do
+          post.last_version_at.to_i.should == revised_at.to_i
+        end
+
+        context "after second window" do
+
+          let!(:new_revised_at) {revised_at + 2.minutes}
+
+          before do
+            subject.revise!(post.user, 'yet another, another updated body', revised_at: new_revised_at)
+            post.reload
+          end
+
+          it "does create a new version after the edit window" do
+            post.cached_version.should == 3
+          end
+
+          it "does create a new version after the edit window" do
+            post.last_version_at.to_i.should == new_revised_at.to_i
+          end
+        end
+      end
+    end
+
+    describe 'rate limiter' do
+      let(:changed_by) { Fabricate(:coding_horror) }
+
+      it "triggers a rate limiter" do
+        EditRateLimiter.any_instance.expects(:performed!)
+        subject.revise!(changed_by, 'updated body')
+      end
+    end
+
+    describe 'with a new body' do
+      let(:changed_by) { Fabricate(:coding_horror) }
+      let!(:result) { subject.revise!(changed_by, 'updated body') }
+
+      it 'returns true' do
+        result.should be_true
+      end
+
+      it 'updates the body' do
+        post.raw.should == 'updated body'
+      end
+
+      it 'sets the invalidate oneboxes attribute' do
+        post.invalidate_oneboxes.should == true
+      end
+
+      it 'increased the cached_version' do
+        post.cached_version.should == 2
+      end
+
+      it 'has the new version in all_versions' do
+        post.all_versions.size.should == 2
+      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
+      end
+
+      context 'second poster posts again quickly' do
+        before do
+          SiteSetting.expects(:ninja_edit_window).returns(1.minute.to_i)
+          subject.revise!(changed_by, 'yet another updated body', revised_at: post.updated_at + 10.seconds)
+          post.reload
+        end
+
+        it 'is a ninja edit, because the second poster posted again quickly' do
+          post.cached_version.should == 2
+        end
+
+        it 'is a ninja edit, because the second poster posted again quickly' do
+          post.all_versions.size.should == 2
+        end
+      end
+    end
+  end
+end
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index c58f44c2b..88c943ec0 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -370,7 +370,7 @@ describe Post do
       let(:changed_by) { Fabricate(:coding_horror) }
       
       it "triggers a rate limiter" do
-        Post::EditRateLimiter.any_instance.expects(:performed!)
+        EditRateLimiter.any_instance.expects(:performed!)
         post.revise(changed_by, 'updated body')
       end
     end