diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 6a3f7386d5b451205aae903042006025ccb26132..c18ea92950615f5b611ce1d4be18f464d1689018 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -141,8 +141,7 @@ $ ->
   $('.trigger-submit').on 'change', ->
     $(@).parents('form').submit()
 
-  $("abbr.timeago").timeago()
-  $('.js-timeago').timeago()
+  $('abbr.timeago, .js-timeago').timeago()
 
   # Flash
   if (flash = $(".flash-container")).length > 0
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 25a7815dba2b3d44707dfd091927705b975faaa4..5c0bc6861117a1c0535a0fc40f205509a7ce3f74 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -1,29 +1,25 @@
 #= require jquery.waitforimages
 #= require task_list
 
+#= require merge_request_tabs
+
 class @MergeRequest
   # Initialize MergeRequest behavior
   #
   # Options:
-  #   action         - String, current controller action
-  #   diffs_loaded   - Boolean, have diffs been pre-rendered server-side?
-  #                    (default: true if `action` is 'diffs', otherwise false)
-  #   commits_loaded - Boolean, have commits been pre-rendered server-side?
-  #                    (default: false)
+  #   action - String, current controller action
   #
   constructor: (@opts) ->
     @initContextWidget()
     this.$el = $('.merge-request')
 
-    @diffs_loaded = @opts.diffs_loaded or @opts.action == 'diffs'
-    @commits_loaded = @opts.commits_loaded or false
-
-    this.bindEvents()
-    this.activateTabFromPath()
-
     this.$('.show-all-commits').on 'click', =>
       this.showAllCommits()
 
+    # `MergeRequests#new` has no tab-persisting or lazy-loading behavior
+    unless @opts.action == 'new'
+      new MergeRequestTabs(@opts)
+
     # Prevent duplicate event bindings
     @disableTaskList()
 
@@ -52,83 +48,6 @@ class @MergeRequest
     $(".context .inline-update").on "change", "#merge_request_assignee_id", ->
       $(this).submit()
 
-
-  bindEvents: ->
-    this.$('.merge-request-tabs a[data-toggle="tab"]').on 'shown.bs.tab', (e) =>
-      $target = $(e.target)
-      tab_action = $target.data('action')
-
-      # Lazy-load diffs
-      if tab_action == 'diffs'
-        this.loadDiff() unless @diffs_loaded
-        $('.diff-header').trigger('sticky_kit:recalc')
-
-      # Skip tab-persisting behavior on MergeRequests#new
-      unless @opts.action == 'new'
-        @setCurrentAction(tab_action)
-
-  # Activate a tab based on the current URL path
-  #
-  # If the current action is 'show' or 'new' (i.e., initial page load),
-  # activates the first tab, otherwise activates the tab corresponding to the
-  # current action (diffs, commits).
-  activateTabFromPath: ->
-    if @opts.action == 'show' || @opts.action == 'new'
-      this.$('.merge-request-tabs a[data-toggle="tab"]:first').tab('show')
-    else
-      this.$(".merge-request-tabs a[data-action='#{@opts.action}']").tab('show')
-
-  # Replaces the current Merge Request-specific action in the URL with a new one
-  #
-  # If the action is "notes", the URL is reset to the standard
-  # `MergeRequests#show` route.
-  #
-  # Examples:
-  #
-  #   location.pathname # => "/namespace/project/merge_requests/1"
-  #   setCurrentAction('diffs')
-  #   location.pathname # => "/namespace/project/merge_requests/1/diffs"
-  #
-  #   location.pathname # => "/namespace/project/merge_requests/1/diffs"
-  #   setCurrentAction('notes')
-  #   location.pathname # => "/namespace/project/merge_requests/1"
-  #
-  #   location.pathname # => "/namespace/project/merge_requests/1/diffs"
-  #   setCurrentAction('commits')
-  #   location.pathname # => "/namespace/project/merge_requests/1/commits"
-  setCurrentAction: (action) ->
-    # Normalize action, just to be safe
-    action = 'notes' if action == 'show'
-
-    # Remove a trailing '/commits' or '/diffs'
-    new_state = location.pathname.replace(/\/(commits|diffs)\/?$/, '')
-
-    # Append the new action if we're on a tab other than 'notes'
-    unless action == 'notes'
-      new_state += "/#{action}"
-
-    # Ensure parameters and hash come along for the ride
-    new_state += location.search + location.hash
-
-    # Replace the current history state with the new one without breaking
-    # Turbolinks' history.
-    #
-    # See https://github.com/rails/turbolinks/issues/363
-    history.replaceState {turbolinks: true, url: new_state}, '', new_state
-
-  loadDiff: (event) ->
-    $.ajax
-      type: 'GET'
-      url: this.$('.merge-request-tabs .diffs-tab a').attr('href') + ".json"
-      beforeSend: =>
-        this.$('.mr-loading-status .loading').show()
-      complete: =>
-        @diffs_loaded = true
-        this.$('.mr-loading-status .loading').hide()
-      success: (data) =>
-        this.$(".diffs").html(data.html)
-      dataType: 'json'
-
   showAllCommits: ->
     this.$('.first-commits').remove()
     this.$('.all-commits').removeClass 'hide'
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..de9a4c2cc2f1d904b372a424cf099730dec4228c
--- /dev/null
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -0,0 +1,153 @@
+# MergeRequestTabs
+#
+# Handles persisting and restoring the current tab selection and lazily-loading
+# content on the MergeRequests#show page.
+#
+# ### Example Markup
+#
+#   <ul class="nav nav-tabs merge-request-tabs">
+#     <li class="notes-tab active">
+#       <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
+#         Discussion
+#       </a>
+#     </li>
+#     <li class="commits-tab">
+#       <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
+#         Commits
+#       </a>
+#     </li>
+#     <li class="diffs-tab">
+#       <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
+#         Diffs
+#       </a>
+#     </li>
+#   </ul>
+#
+#   <div class="tab-content">
+#     <div class="notes tab-pane active" id="notes">
+#       Notes Content
+#     </div>
+#     <div class="commits tab-pane" id="commits">
+#       Commits Content
+#     </div>
+#     <div class="diffs tab-pane" id="diffs">
+#       Diffs Content
+#     </div>
+#   </div>
+#
+#   <div class="mr-loading-status">
+#     <div class="loading">
+#       Loading Animation
+#     </div>
+#   </div>
+#
+class @MergeRequestTabs
+  diffsLoaded: false
+  commitsLoaded: false
+
+  constructor: (@opts = {}) ->
+    # Store the `location` object, allowing for easier stubbing in tests
+    @_location = location
+
+    @bindEvents()
+    @activateTab(@opts.action)
+
+    switch @opts.action
+      when 'commits' then @commitsLoaded = true
+      when 'diffs'   then @diffsLoaded = true
+
+  bindEvents: ->
+    $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown
+
+  tabShown: (event) =>
+    $target = $(event.target)
+    action = $target.data('action')
+
+    if action == 'commits'
+      @loadCommits($target.attr('href'))
+    else if action == 'diffs'
+      @loadDiff($target.attr('href'))
+
+    @setCurrentAction(action)
+
+  # Activate a tab based on the current action
+  activateTab: (action) ->
+    action = 'notes' if action == 'show'
+    $(".merge-request-tabs a[data-action='#{action}']").tab('show')
+
+  # Replaces the current Merge Request-specific action in the URL with a new one
+  #
+  # If the action is "notes", the URL is reset to the standard
+  # `MergeRequests#show` route.
+  #
+  # Examples:
+  #
+  #   location.pathname # => "/namespace/project/merge_requests/1"
+  #   setCurrentAction('diffs')
+  #   location.pathname # => "/namespace/project/merge_requests/1/diffs"
+  #
+  #   location.pathname # => "/namespace/project/merge_requests/1/diffs"
+  #   setCurrentAction('notes')
+  #   location.pathname # => "/namespace/project/merge_requests/1"
+  #
+  #   location.pathname # => "/namespace/project/merge_requests/1/diffs"
+  #   setCurrentAction('commits')
+  #   location.pathname # => "/namespace/project/merge_requests/1/commits"
+  #
+  # Returns the new URL String
+  setCurrentAction: (action) =>
+    # Normalize action, just to be safe
+    action = 'notes' if action == 'show'
+
+    # Remove a trailing '/commits' or '/diffs'
+    new_state = @_location.pathname.replace(/\/(commits|diffs)\/?$/, '')
+
+    # Append the new action if we're on a tab other than 'notes'
+    unless action == 'notes'
+      new_state += "/#{action}"
+
+    # Ensure parameters and hash come along for the ride
+    new_state += @_location.search + @_location.hash
+
+    # Replace the current history state with the new one without breaking
+    # Turbolinks' history.
+    #
+    # See https://github.com/rails/turbolinks/issues/363
+    history.replaceState {turbolinks: true, url: new_state}, document.title, new_state
+
+    new_state
+
+  loadCommits: (source) ->
+    return if @commitsLoaded
+
+    @_get
+      url: "#{source}.json"
+      success: (data) =>
+        document.getElementById('commits').innerHTML = data.html
+        $('.js-timeago').timeago()
+        @commitsLoaded = true
+
+  loadDiff: (source) ->
+    return if @diffsLoaded
+
+    @_get
+      url: "#{source}.json"
+      success: (data) =>
+        document.getElementById('diffs').innerHTML = data.html
+        $('.diff-header').trigger('sticky_kit:recalc')
+        @diffsLoaded = true
+
+  toggleLoading: ->
+    $('.mr-loading-status .loading').toggle()
+
+  _get: (options) ->
+    defaults = {
+      beforeSend: @toggleLoading
+      complete: @toggleLoading
+      dataType: 'json'
+      type: 'GET'
+    }
+
+    options = $.extend({}, defaults, options)
+
+    $.ajax(options)
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 14069bafe71eb5d08c65037fa2069a8651910f3f..51ecbfd561aecdad6cb2d761dabd88b5895e43a6 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -71,7 +71,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   end
 
   def commits
-    render 'show'
+    respond_to do |format|
+      format.html { render 'show' }
+      format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } }
+    end
   end
 
   def new
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 10d7aa11209f98b4e61a29f7646993e2045ff393..c1342331119fe90b28956e9dd56183bd1977652f 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -179,14 +179,33 @@ module ApplicationHelper
     BroadcastMessage.current
   end
 
-  def time_ago_with_tooltip(date, placement = 'top', html_class = 'time_ago')
-    capture_haml do
-      haml_tag :time, date.to_s,
-        class: html_class, datetime: date.getutc.iso8601, title: date.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
-        data: { toggle: 'tooltip', placement: placement }
-
-      haml_tag :script, "$('." + html_class + "').timeago().tooltip()"
-    end.html_safe
+  # Render a `time` element with Javascript-based relative date and tooltip
+  #
+  # time       - Time object
+  # placement  - Tooltip placement String (default: "top")
+  # html_class - Custom class for `time` element (default: "time_ago")
+  # skip_js    - When true, exclude the `script` tag (default: false)
+  #
+  # By default also includes a `script` element with Javascript necessary to
+  # initialize the `timeago` jQuery extension. If this method is called many
+  # times, for example rendering hundreds of commits, it's advisable to disable
+  # this behavior using the `skip_js` argument and re-initializing `timeago`
+  # manually once all of the elements have been rendered.
+  #
+  # A `js-timeago` class is always added to the element, even when a custom
+  # `html_class` argument is provided.
+  #
+  # Returns an HTML-safe String
+  def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
+    element = content_tag :time, time.to_s,
+      class: "#{html_class} js-timeago",
+      datetime: time.getutc.iso8601,
+      title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
+      data: { toggle: 'tooltip', placement: placement }
+
+    element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
+
+    element
   end
 
   def render_markup(file_name, file_content)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 36d3f371c1b69408a36df65180523ef104780dd8..d4c345fe43173e28e695892833a84d84873f34d4 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -45,13 +45,13 @@ module IssuesHelper
 
   def issue_timestamp(issue)
     # Shows the created at time and the updated at time if different
-    ts = "#{time_ago_with_tooltip(issue.created_at, 'bottom', 'note_created_ago')}"
+    ts = time_ago_with_tooltip(issue.created_at, placement: 'bottom', html_class: 'note_created_ago')
     if issue.updated_at != issue.created_at
       ts << capture_haml do
         haml_tag :span do
           haml_concat '&middot;'
           haml_concat icon('edit', title: 'edited')
-          haml_concat time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_edited_ago')
+          haml_concat time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
         end
       end
     end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index a7c1fa0b071fd91cca079d7fd63cc3ea661612ae..dda9b17d61dbbd8780b1ac3d719c1d471d4527a7 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -25,13 +25,13 @@ module NotesHelper
 
   def note_timestamp(note)
     # Shows the created at time and the updated at time if different
-    ts = "#{time_ago_with_tooltip(note.created_at, 'bottom', 'note_created_ago')}"
+    ts = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note_created_ago')
     if note.updated_at != note.created_at
       ts << capture_haml do
         haml_tag :span do
           haml_concat '&middot;'
           haml_concat icon('edit', title: 'edited')
-          haml_concat time_ago_with_tooltip(note.updated_at, 'bottom', 'note_edited_ago')
+          haml_concat time_ago_with_tooltip(note.updated_at, placement: 'bottom', html_class: 'note_edited_ago')
         end
       end
     end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 94ce6646634adcd70cf898e97108dc64c3643cd7..ec65e473919a29659bf7cb50051cb5b328da2833 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -211,7 +211,7 @@ module ProjectsHelper
 
   def project_last_activity(project)
     if project.last_activity_at
-      time_ago_with_tooltip(project.last_activity_at, 'bottom', 'last_activity_time_ago')
+      time_ago_with_tooltip(project.last_activity_at, placement: 'bottom', html_class: 'last_activity_time_ago')
     else
       "Never"
     end
diff --git a/app/models/user.rb b/app/models/user.rb
index a2e2d220b3a7d187333847d4b6c59f01a03be3ee..29f430514641ae844722ed6c802375c028a2e8f9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -223,10 +223,26 @@ class User < ActiveRecord::Base
     end
 
     def find_for_commit(email, name)
-      # Prefer email match over name match
-      User.where(email: email).first ||
-        User.joins(:emails).where(emails: { email: email }).first ||
-        User.where(name: name).first
+      user_table = arel_table
+      email_table = Email.arel_table
+
+      # Use ARel to build a query:
+      query = user_table.
+        # SELECT "users".* FROM "users"
+        project(user_table[Arel.star]).
+        # LEFT OUTER JOIN "emails"
+        join(email_table, Arel::Nodes::OuterJoin).
+        # ON "users"."id" = "emails"."user_id"
+        on(user_table[:id].eq(email_table[:user_id])).
+        # WHERE ("user"."email" = '<email>' OR "user"."name" = '<name>')
+        # OR "emails"."email" = '<email>'
+        where(
+          user_table[:email].eq(email).
+          or(user_table[:name].eq(name)).
+          or(email_table[:email].eq(email))
+        )
+
+      find_by_sql(query.to_sql).first
     end
 
     def filter(filter_name)
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 083fca9b6582fbeeff93ec3f9060e3dca1b678fe..f9106564a276032545d312b818edf52795503509 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -29,5 +29,5 @@
     = commit_author_link(commit, avatar: true, size: 24)
     authored
     .committed_ago
-      #{time_ago_with_tooltip(commit.committed_date)} &nbsp;
+      #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} &nbsp;
     = link_to_browse_code(project, commit)
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 64d62b456579a716c5ff2a5c2de8a220060c0801..2c296cab9771b715e037ac04c6d21e7fc1abbd40 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -28,7 +28,7 @@
           = 0
 
   .issue-info
-    = "##{issue.iid} opened #{time_ago_with_tooltip(issue.created_at, 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe
+    = "##{issue.iid} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe
     - if issue.votes_count > 0
       = render 'votes/votes_inline', votable: issue
     - if issue.milestone
@@ -41,4 +41,4 @@
         = issue.task_status
 
     .pull-right.issue-updated-at
-      %small updated #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_update_ago')}
+      %small updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index c16df27ee8fed811bd38dfbf064c14150808a118..d79c45482473d65a0a154add9fb0d03c5d444881 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -35,7 +35,7 @@
           = 0
 
   .merge-request-info
-    = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe
+    = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe
     - if merge_request.votes_count > 0
       = render 'votes/votes_inline', votable: merge_request
     - if merge_request.milestone_id?
@@ -48,4 +48,4 @@
         = merge_request.task_status
 
     .pull-right.hidden-xs
-      %small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')}
+      %small updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 5d7e73f2b2838905861a6e4145796b2e605e4185..9dc4a47258e0f564f1c3d7da9b8bcee4816b590b 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -56,7 +56,8 @@
     #notes.notes.tab-pane.voting_notes
       = render "projects/merge_requests/discussion"
     #commits.commits.tab-pane
-      = render "projects/merge_requests/show/commits"
+      - if current_page?(action: 'commits')
+        = render "projects/merge_requests/show/commits"
     #diffs.diffs.tab-pane
       - if current_page?(action: 'diffs')
         = render "projects/merge_requests/show/diffs"
@@ -64,7 +65,6 @@
   .mr-loading-status
     = spinner
 
-
 :javascript
   var merge_request;
 
diff --git a/app/views/projects/notes/discussions/_active.html.haml b/app/views/projects/notes/discussions/_active.html.haml
index e7a3854701c3c96ffb689c3f465297877b503ade..4f15a99d0610bc4c54bcdf7e8c67cc69758e0a09 100644
--- a/app/views/projects/notes/discussions/_active.html.haml
+++ b/app/views/projects/notes/discussions/_active.html.haml
@@ -16,7 +16,7 @@
       = link_to_member(@project, last_note.author, avatar: false)
 
       %span.discussion-last-update
-        #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')}
-        
+        #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
+
   .discussion-body.js-toggle-content
     = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/notes/discussions/_commit.html.haml b/app/views/projects/notes/discussions/_commit.html.haml
index 62609cfc1c8858d395a2e91b61c0921aaf0d7606..6903fad4a0aa53ec20be59613334e6746a8561af 100644
--- a/app/views/projects/notes/discussions/_commit.html.haml
+++ b/app/views/projects/notes/discussions/_commit.html.haml
@@ -14,7 +14,7 @@
       last updated by
       = link_to_member(@project, last_note.author, avatar: false)
       %span.discussion-last-update
-        #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')}
+        #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
   .discussion-body.js-toggle-content
     - if note.for_diff_line?
       = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/app/views/projects/notes/discussions/_outdated.html.haml b/app/views/projects/notes/discussions/_outdated.html.haml
index 52a1d342f55059f6be65b5089027ec4335664be8..218b0da3977995aa3fe1b2fe77e78da6f280cec1 100644
--- a/app/views/projects/notes/discussions/_outdated.html.haml
+++ b/app/views/projects/notes/discussions/_outdated.html.haml
@@ -14,6 +14,6 @@
       last updated by
       = link_to_member(@project, last_note.author, avatar: false)
       %span.discussion-last-update
-        #{time_ago_with_tooltip(last_note.updated_at, 'bottom', 'discussion_updated_ago')}
+        #{time_ago_with_tooltip(last_note.updated_at, placement: 'bottom', html_class: 'discussion_updated_ago')}
   .discussion-body.js-toggle-content.hide
     = render "projects/notes/discussions/diff", discussion_notes: discussion_notes, note: note
diff --git a/spec/controllers/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
similarity index 58%
rename from spec/controllers/merge_requests_controller_spec.rb
rename to spec/controllers/projects/merge_requests_controller_spec.rb
index c94ef1629ae5cfd4ea001bf1c04c5ddc7faa298d..5cc5bd78cd72ebc48c09b50fc5ca205cbe3bc4e1 100644
--- a/spec/controllers/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -79,23 +79,72 @@ describe Projects::MergeRequestsController do
     end
   end
 
-  context '#diffs with forked projects with submodules' do
-    render_views
-    let(:project) { create(:project) }
-    let(:fork_project) { create(:forked_project_with_submodules) }
-    let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
-
-    before do
-      fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
-      fork_project.save
-      merge_request.reload
+  describe 'GET diffs' do
+    def go(format: 'html')
+      get :diffs, namespace_id: project.namespace.to_param,
+        project_id: project.to_param, id: merge_request.iid, format: format
     end
 
-    it '#diffs' do
-      get(:diffs, namespace_id: project.namespace.to_param,
-          project_id: project.to_param, id: merge_request.iid, format: 'json')
-      expect(response).to be_success
-      expect(response.body).to have_content('Subproject commit')
+    context 'as html' do
+      it 'renders the diff template' do
+        go
+
+        expect(response).to render_template('diffs')
+      end
+    end
+
+    context 'as json' do
+      it 'renders the diffs template to a string' do
+        go format: 'json'
+
+        expect(response).to render_template('projects/merge_requests/show/_diffs')
+        expect(JSON.parse(response.body)).to have_key('html')
+      end
+    end
+
+    context 'with forked projects with submodules' do
+      render_views
+
+      let(:project) { create(:project) }
+      let(:fork_project) { create(:forked_project_with_submodules) }
+      let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+
+      before do
+        fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+        fork_project.save
+        merge_request.reload
+      end
+
+      it 'renders' do
+        go format: 'json'
+
+        expect(response).to be_success
+        expect(response.body).to have_content('Subproject commit')
+      end
+    end
+  end
+
+  describe 'GET commits' do
+    def go(format: 'html')
+      get :commits, namespace_id: project.namespace.to_param,
+        project_id: project.to_param, id: merge_request.iid, format: format
+    end
+
+    context 'as html' do
+      it 'renders the show template' do
+        go
+
+        expect(response).to render_template('show')
+      end
+    end
+
+    context 'as json' do
+      it 'renders the commits template to a string' do
+        go format: 'json'
+
+        expect(response).to render_template('projects/merge_requests/show/_commits')
+        expect(JSON.parse(response.body)).to have_key('html')
+      end
     end
   end
 end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 47e10197f5c4dac6c4ed571aba34e372e7f83f9d..a62c801e6a2556353b370b9fa147bf223045db6d 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -240,6 +240,55 @@ describe ApplicationHelper do
     end
   end
 
+  describe 'time_ago_with_tooltip' do
+    def element(*arguments)
+      Time.zone = 'UTC'
+      time = Time.zone.parse('2015-07-02 08:00')
+      element = time_ago_with_tooltip(time, *arguments)
+
+      Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
+    end
+
+    it 'returns a time element' do
+      expect(element.name).to eq 'time'
+    end
+
+    it 'includes the date string' do
+      expect(element.text).to eq '2015-07-02 08:00:00 UTC'
+    end
+
+    it 'has a datetime attribute' do
+      expect(element.attr('datetime')).to eq '2015-07-02T08:00:00Z'
+    end
+
+    it 'has a formatted title attribute' do
+      expect(element.attr('title')).to eq 'Jul 02, 2015 8:00am'
+    end
+
+    it 'includes a default js-timeago class' do
+      expect(element.attr('class')).to eq 'time_ago js-timeago'
+    end
+
+    it 'accepts a custom html_class' do
+      expect(element(html_class: 'custom_class').attr('class')).to eq 'custom_class js-timeago'
+    end
+
+    it 'accepts a custom tooltip placement' do
+      expect(element(placement: 'bottom').attr('data-placement')).to eq 'bottom'
+    end
+
+    it 're-initializes timeago Javascript' do
+      el = element.next_element
+
+      expect(el.name).to eq 'script'
+      expect(el.text).to include "$('.js-timeago').timeago()"
+    end
+
+    it 'allows the script tag to be excluded' do
+      expect(element(skip_js: true)).not_to include 'script'
+    end
+  end
+
   describe 'render_markup' do
     let(:content) { 'No毛l' }
 
diff --git a/spec/javascripts/fixtures/merge_request_tabs.html.haml b/spec/javascripts/fixtures/merge_request_tabs.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..7624a713948aeea7de1a67e91b888727a275f9a2
--- /dev/null
+++ b/spec/javascripts/fixtures/merge_request_tabs.html.haml
@@ -0,0 +1,22 @@
+%ul.nav.nav-tabs.merge-request-tabs
+  %li.notes-tab
+    %a{href: '/foo/bar/merge_requests/1', data: {target: '#notes', action: 'notes', toggle: 'tab'}}
+      Discussion
+  %li.commits-tab
+    %a{href: '/foo/bar/merge_requests/1/commits', data: {target: '#commits', action: 'commits', toggle: 'tab'}}
+      Commits
+  %li.diffs-tab
+    %a{href: '/foo/bar/merge_requests/1/diffs', data: {target: '#diffs', action: 'diffs', toggle: 'tab'}}
+      Diffs
+
+.tab-content
+  #notes.notes.tab-pane
+    Notes Content
+  #commits.commits.tab-pane
+    Commits Content
+  #diffs.diffs.tab-pane
+    Diffs Content
+
+.mr-loading-status
+  .loading
+    Loading Animation
diff --git a/spec/javascripts/merge_request_tabs_spec.js.coffee b/spec/javascripts/merge_request_tabs_spec.js.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..6cc96fb68a0aed9e65356e7d28833e92b1fec48e
--- /dev/null
+++ b/spec/javascripts/merge_request_tabs_spec.js.coffee
@@ -0,0 +1,82 @@
+#= require merge_request_tabs
+
+describe 'MergeRequestTabs', ->
+  stubLocation = (stubs) ->
+    defaults = {pathname: '', search: '', hash: ''}
+    $.extend(defaults, stubs)
+
+  fixture.preload('merge_request_tabs.html')
+
+  beforeEach ->
+    @class = new MergeRequestTabs()
+    @spies = {
+      ajax:    spyOn($, 'ajax').and.callFake ->
+      history: spyOn(history, 'replaceState').and.callFake ->
+    }
+
+  describe '#activateTab', ->
+    beforeEach ->
+      fixture.load('merge_request_tabs.html')
+      @subject = @class.activateTab
+
+    it 'shows the first tab when action is show', ->
+      @subject('show')
+      expect($('#notes')).toHaveClass('active')
+
+    it 'shows the notes tab when action is notes', ->
+      @subject('notes')
+      expect($('#notes')).toHaveClass('active')
+
+    it 'shows the commits tab when action is commits', ->
+      @subject('commits')
+      expect($('#commits')).toHaveClass('active')
+
+    it 'shows the diffs tab when action is diffs', ->
+      @subject('diffs')
+      expect($('#diffs')).toHaveClass('active')
+
+  describe '#setCurrentAction', ->
+    beforeEach ->
+      @subject = @class.setCurrentAction
+
+    it 'changes from commits', ->
+      @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits')
+
+      expect(@subject('notes')).toBe('/foo/bar/merge_requests/1')
+      expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs')
+
+    it 'changes from diffs', ->
+      @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs')
+
+      expect(@subject('notes')).toBe('/foo/bar/merge_requests/1')
+      expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits')
+
+    it 'changes from notes', ->
+      @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1')
+
+      expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs')
+      expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits')
+
+    it 'includes search parameters and hash string', ->
+      @class._location = stubLocation({
+        pathname: '/foo/bar/merge_requests/1/diffs'
+        search:   '?view=parallel'
+        hash:     '#L15-35'
+      })
+
+      expect(@subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35')
+
+    it 'replaces the current history state', ->
+      @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1')
+      new_state = @subject('commits')
+
+      expect(@spies.history).toHaveBeenCalledWith(
+        {turbolinks: true, url: new_state},
+        document.title,
+        new_state
+      )
+
+    it 'treats "show" like "notes"', ->
+      @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits')
+
+      expect(@subject('show')).toBe('/foo/bar/merge_requests/1')
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index fa7680fbbec697df3633deaae1eb94ce701b403b..afdd9f71af208416dbfc651712aa77cd4cb96972 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -364,6 +364,31 @@ describe User do
     end
   end
 
+  describe '.find_for_commit' do
+    it 'finds by primary email' do
+      user = create(:user, email: 'foo@example.com')
+
+      expect(User.find_for_commit(user.email, '')).to eq user
+    end
+
+    it 'finds by secondary email' do
+      email = create(:email, email: 'foo@example.com')
+      user  = email.user
+
+      expect(User.find_for_commit(email.email, '')).to eq user
+    end
+
+    it 'finds by name' do
+      user = create(:user, name: 'Joey JoJo')
+
+      expect(User.find_for_commit('', 'Joey JoJo')).to eq user
+    end
+
+    it 'returns nil when nothing found' do
+      expect(User.find_for_commit('', '')).to be_nil
+    end
+  end
+
   describe 'search' do
     let(:user1) { create(:user, username: 'James', email: 'james@testing.com') }
     let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') }