diff --git a/.csscomb.json b/.csscomb.json
index e353e6a63d0ed22a85b806c045278adfb04d22c7..741cc1488b5567ceda1b6f650eb2561429acc9d4 100644
--- a/.csscomb.json
+++ b/.csscomb.json
@@ -1,16 +1,20 @@
 {
-    "always-semicolon": true,
-    "color-case": "lower",
-    "block-indent": "  ",
-    "color-shorthand": true,
-    "element-case": "lower",
-    "space-before-colon": "",
-    "space-after-colon": " ",
-    "space-before-combinator": " ",
-    "space-after-combinator": " ",
-    "space-between-declarations": "\n",
-    "space-before-opening-brace": " ",
-    "space-after-opening-brace": "\n",
-    "space-before-closing-brace": "\n",
-    "unitless-zero": true
+  "exclude": [
+    "app/assets/stylesheets/framework/tw_bootstrap_variables.scss",
+    "app/assets/stylesheets/framework/fonts.scss"
+  ],
+  "always-semicolon": true,
+  "color-case": "lower",
+  "block-indent": "  ",
+  "color-shorthand": true,
+  "element-case": "lower",
+  "space-before-colon": "",
+  "space-after-colon": " ",
+  "space-before-combinator": " ",
+  "space-after-combinator": " ",
+  "space-between-declarations": "\n",
+  "space-before-opening-brace": " ",
+  "space-after-opening-brace": "\n",
+  "space-before-closing-brace": "\n",
+  "unitless-zero": true
 }
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2ad63548d78dec8c4ac7ca22ae95bee4d2d7d4b9..53f115c92c89e1b27d21f1afeca712ab33d5df99 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -128,7 +128,6 @@ scss-lint:
     - bundle exec rake scss_lint
   tags:
     - ruby
-  allow_failure: true
 
 brakeman:
   stage: test
diff --git a/.scss-lint.yml b/.scss-lint.yml
index e350b2073c3133be6e75a30f6f28b322b73efa66..937d3407b60696fdd3fc2d60c17a5828af620984 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -100,7 +100,7 @@ linters:
   # Selectors should always use hyphenated-lowercase, rather than camelCase or
   # snake_case.
   SelectorFormat:
-    enabled: true
+    enabled: false
     convention: hyphenated_lowercase
   
   # Prefer the shortest shorthand form possible for properties that support it.
diff --git a/CHANGELOG b/CHANGELOG
index 20a21abfb69b9cec9feaf5b78b2ced81b9c9f8a8..7e9a447a8f6f229ff009b028b4cb02477bea77e7 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,10 +1,36 @@
 Please view this file on the master branch, on stable branches it's out of date.
 
-v 8.6.0 (unreleased)
+v 8.7.0 (unreleased)
+  - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan hu)
+  - Preserve time notes/comments have been updated at when moving issue
+  - Make HTTP(s) label consistent on clone bar (Stan Hu)
+  - Fix avatar stretching by providing a cropping feature
+  - Add links to CI setup documentation from project settings and builds pages
+  - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
+  - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
+
+v 8.6.2 (unreleased)
+  - Comments on confidential issues don't show up in activity feed to non-members
+
+v 8.6.1
+  - Add option to reload the schema before restoring a database backup. !2807
+  - Display navigation controls on mobile. !3214
+  - Fixed bug where participants would not work correctly on merge requests. !3329
+  - Fix sorting issues by votes on the groups issues page results in SQL errors. !3333
+  - Restrict notifications for confidential issues. !3334
+  - Do not allow to move issue if it has not been persisted. !3340
+  - Add a confirmation step before deleting an issuable. !3341
+  - Fixes issue with signin button overflowing on mobile. !3342
+  - Auto collapses the navigation sidebar when resizing. !3343
+  - Fix build dependencies, when the dependency is a string. !3344
+  - Shows error messages when trying to create label in dropdown menu. !3345
+  - Fixes issue with assign milestone not loading milestone list. !3346
+  - Fix an issue causing the Dashboard/Milestones page to be blank. !3348
+
+v 8.6.0
   - Add ability to move issue to another project
   - Prevent tokens in the import URL to be showed by the UI
   - Fix bug where wrong commit ID was being used in a merge request diff to show old image (Stan Hu)
-  - Make HTTP(s) label consistent on clone bar (Stan Hu)
   - Add confidential issues
   - Bump gitlab_git to 9.0.3 (Stan Hu)
   - Fix diff image view modes (2-up, swipe, onion skin) not working (Stan Hu)
@@ -19,9 +45,11 @@ v 8.6.0 (unreleased)
     setup. A password can be provided during setup (see installation docs), or
     GitLab will ask the user to create a new one upon first visit.
   - Fix issue when pushing to projects ending in .wiki
+  - Properly display YAML front matter in Markdown
   - Add support for wiki with UTF-8 page names (Hiroyuki Sato)
   - Fix wiki search results point to raw source (Hiroyuki Sato)
   - Don't load all of GitLab in mail_room
+  - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner)
   - HTTP error pages work independently from location and config (Artem Sidorenko)
   - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
   - Memoize @group in Admin::GroupsController (Yatish Mehta)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7540fa1afccf417ecd4c5043803e6045e72e9b00..511336f384ce7ca415bbf3543f28757780985233 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -16,6 +16,7 @@
         - [Issue tracker guidelines](#issue-tracker-guidelines)
         - [Issue weight](#issue-weight)
         - [Regression issues](#regression-issues)
+        - [Technical debt](#technical-debt)
     - [Merge requests](#merge-requests)
         - [Merge request guidelines](#merge-request-guidelines)
         - [Merge request description format](#merge-request-description-format)
@@ -242,6 +243,28 @@ addressed.
 [8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
 [update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
 
+### Technical debt
+
+In order to track things that can be improved in GitLab's codebase, we created
+the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
+
+This label should be added to issues that describe things that can be improved,
+shortcuts that have been taken, code that needs refactoring, features that need
+additional attention, and all other things that have been left behind due to
+high velocity of development.
+
+Everyone can create an issue, though you may need to ask for adding a specific
+label, if you do not have permissions to do it by yourself. Additional labels
+can be combined with the `technical debt` label, to make it easier to schedule
+the improvements for a release.
+
+Issues tagged with the `technical debt` label have the same priority like issues
+that describe a new feature to be introduced in GitLab, and should be scheduled
+for a release by the appropriate person.
+
+Make sure to mention the merge request that the `technical debt` issue is
+associated with in the description of the issue.
+
 ## Merge requests
 
 We welcome merge requests with fixes and improvements to GitLab code, tests,
diff --git a/Gemfile b/Gemfile
index cebd76e7370609d5f1a4333ca1f939db0b4a4e54..006e53e0c100cac2b359d11f4d41e4c565481f45 100644
--- a/Gemfile
+++ b/Gemfile
@@ -234,7 +234,7 @@ end
 
 group :development do
   gem "foreman"
-  gem 'brakeman', '~> 3.1.0', require: false
+  gem 'brakeman', '~> 3.2.0', require: false
 
   gem "annotate", "~> 2.6.0"
   gem "letter_opener", '~> 1.1.2'
@@ -279,7 +279,7 @@ group :development, :test do
   gem 'capybara-screenshot', '~> 1.0.0'
   gem 'poltergeist',         '~> 1.9.0'
 
-  gem 'teaspoon', '~> 1.0.0'
+  gem 'teaspoon', '~> 1.1.0'
   gem 'teaspoon-jasmine', '~> 2.2.0'
 
   gem 'spring', '~> 1.6.4'
diff --git a/Gemfile.lock b/Gemfile.lock
index 16c09ab6e6d849b7360eada016d31405055aeeb9..da27c62acbf10db379533f301c9afcc095effbce 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -84,21 +84,19 @@ GEM
     bootstrap-sass (3.3.6)
       autoprefixer-rails (>= 5.2.1)
       sass (>= 3.3.4)
-    brakeman (3.1.4)
+    brakeman (3.2.1)
       erubis (~> 2.6)
-      fastercsv (~> 1.5)
       haml (>= 3.0, < 5.0)
       highline (>= 1.6.20, < 2.0)
-      multi_json (~> 1.2)
-      ruby2ruby (>= 2.1.1, < 2.3.0)
-      ruby_parser (~> 3.7.0)
+      ruby2ruby (~> 2.3.0)
+      ruby_parser (~> 3.8.1)
       safe_yaml (>= 1.0)
       sass (~> 3.0)
       slim (>= 1.3.6, < 4.0)
       terminal-table (~> 1.4)
     browser (1.0.1)
     builder (3.2.2)
-    bullet (4.14.10)
+    bullet (5.0.0)
       activesupport (>= 3.0.0)
       uniform_notifier (~> 1.9.0)
     bundler-audit (0.4.0)
@@ -208,7 +206,6 @@ GEM
     faraday_middleware-multi_json (0.0.6)
       faraday_middleware
       multi_json
-    fastercsv (1.5.5)
     ffaker (2.0.0)
     ffi (1.9.10)
     fission (0.5.0)
@@ -328,8 +325,8 @@ GEM
     fog-xml (0.1.2)
       fog-core
       nokogiri (~> 1.5, >= 1.5.11)
-    font-awesome-rails (4.5.0.0)
-      railties (>= 3.2, < 5.0)
+    font-awesome-rails (4.5.0.1)
+      railties (>= 3.2, < 5.1)
     foreman (0.78.0)
       thor (~> 0.19.1)
     formatador (0.2.5)
@@ -706,10 +703,10 @@ GEM
     ruby-saml (1.1.2)
       nokogiri (>= 1.5.10)
       uuid (~> 2.3)
-    ruby2ruby (2.2.0)
+    ruby2ruby (2.3.0)
       ruby_parser (~> 3.1)
       sexp_processor (~> 4.0)
-    ruby_parser (3.7.2)
+    ruby_parser (3.8.1)
       sexp_processor (~> 4.1)
     rubyntlm (0.5.2)
     rubypants (0.2.0)
@@ -718,7 +715,7 @@ GEM
     safe_yaml (1.0.4)
     sanitize (2.1.0)
       nokogiri (>= 1.4.4)
-    sass (3.4.20)
+    sass (3.4.21)
     sass-rails (5.0.4)
       railties (>= 4.0.0, < 5.0)
       sass (~> 3.1)
@@ -742,7 +739,7 @@ GEM
     sentry-raven (0.15.6)
       faraday (>= 0.7.6)
     settingslogic (2.0.9)
-    sexp_processor (4.6.0)
+    sexp_processor (4.7.0)
     sham_rack (1.3.6)
       rack
     shoulda-matchers (2.8.0)
@@ -806,8 +803,8 @@ GEM
     systemu (2.6.5)
     task_list (1.0.2)
       html-pipeline
-    teaspoon (1.0.2)
-      railties (>= 3.2.5, < 5)
+    teaspoon (1.1.5)
+      railties (>= 3.2.5, < 6)
     teaspoon-jasmine (2.2.0)
       teaspoon (>= 1.0.0)
     temple (0.7.6)
@@ -868,7 +865,7 @@ GEM
       equalizer (~> 0.0, >= 0.0.9)
     warden (1.2.4)
       rack (>= 1.0)
-    web-console (2.2.1)
+    web-console (2.3.0)
       activemodel (>= 4.0)
       binding_of_caller (>= 0.7.2)
       railties (>= 4.0)
@@ -910,7 +907,7 @@ DEPENDENCIES
   better_errors (~> 1.0.1)
   binding_of_caller (~> 0.7.2)
   bootstrap-sass (~> 3.3.0)
-  brakeman (~> 3.1.0)
+  brakeman (~> 3.2.0)
   browser (~> 1.0.0)
   bullet
   bundler-audit
@@ -1048,7 +1045,7 @@ DEPENDENCIES
   sprockets (~> 3.3.5)
   state_machines-activerecord (~> 0.3.0)
   task_list (~> 1.0.2)
-  teaspoon (~> 1.0.0)
+  teaspoon (~> 1.1.0)
   teaspoon-jasmine (~> 2.2.0)
   test_after_commit (~> 0.4.2)
   thin (~> 1.6.1)
@@ -1064,3 +1061,6 @@ DEPENDENCIES
   web-console (~> 2.0)
   webmock (~> 1.21.0)
   wikicloth (= 0.8.1)
+
+BUNDLED WITH
+   1.11.2
diff --git a/VERSION b/VERSION
index cac7d91adda628435e13bb2c00bf6dd24de15c0a..91ab1f99daf48de9fb27d5b76b81571cf2ec2ff4 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.6.0-pre
+8.7.0-pre
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 2ddf8612db30ac4b36ddc18d4b1fb52a86b35cee..f3ed9a66715fe40588441ec74caadb2fcb52738a 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -74,6 +74,8 @@
       dataType: "json"
     ).done (label) ->
       callback(label)
+    .error (message) ->
+      callback(message.responseJSON)
 
   # Return group projects list. Filtered by query
   groupProjects: (group_id, query, callback) ->
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 01451830653174d7893e2acccd89713c08f51eb8..f01c67e9474fa066dc7a3b004c880ce3c8dc328e 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -43,6 +43,7 @@
 #= require jquery.nicescroll
 #= require_tree .
 #= require fuzzaldrin-plus
+#= require cropper
 
 window.slugify = (text) ->
   text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
@@ -218,13 +219,20 @@ $ ->
       $this = $(this)
       $this.attr 'value', $this.val()
 
+  $sidebarGutterToggle = $('.js-sidebar-toggle')
+  $navIconToggle = $('.toggle-nav-collapse')
+
   $(document)
     .off 'breakpoint:change'
     .on 'breakpoint:change', (e, breakpoint) ->
       if breakpoint is 'sm' or breakpoint is 'xs'
-        $gutterIcon = $('.js-sidebar-toggle').find('i')
+        $gutterIcon = $sidebarGutterToggle.find('i')
         if $gutterIcon.hasClass('fa-angle-double-right')
-          $gutterIcon.closest('a').trigger('click')
+          $sidebarGutterToggle.trigger('click')
+
+        $navIcon = $navIconToggle.find('.fa')
+        if $navIcon.hasClass('fa-angle-left')
+          $navIconToggle.trigger('click')
 
   $(document)
     .off 'click', '.js-sidebar-toggle'
diff --git a/app/assets/javascripts/gl_crop.js.coffee b/app/assets/javascripts/gl_crop.js.coffee
new file mode 100644
index 0000000000000000000000000000000000000000..df9bfdfa6cc7c5988195d87af5d7b93ddc9e8220
--- /dev/null
+++ b/app/assets/javascripts/gl_crop.js.coffee
@@ -0,0 +1,152 @@
+class GitLabCrop
+  # Matches everything but the file name
+  FILENAMEREGEX = /^.*[\\\/]/
+
+  constructor: (input, opts = {}) ->
+    @fileInput = $(input)
+
+    # We should rename to avoid spec to fail
+    # Form will submit the proper input filed with a file using FormData
+    @fileInput
+      .attr('name', "#{@fileInput.attr('name')}-trigger")
+      .attr('id', "#{@fileInput.attr('id')}-trigger")
+
+    # Set defaults
+    {
+      @exportWidth = 200
+      @exportHeight = 200
+      @cropBoxWidth = 200
+      @cropBoxHeight = 200
+      @form = @fileInput.parents('form')
+
+      # Required params
+      @filename
+      @previewImage
+      @modalCrop
+      @pickImageEl
+      @uploadImageBtn
+      @modalCropImg
+    } = opts
+
+    # Ensure needed elements are jquery objects
+    # If selector is provided we will convert them to a jQuery Object
+    @filename = @getElement(@filename)
+    @previewImage = @getElement(@previewImage)
+    @pickImageEl = @getElement(@pickImageEl)
+
+    # Modal elements usually are outside the @form element
+    @modalCrop = if _.isString(@modalCrop) then $(@modalCrop) else @modalCrop
+    @uploadImageBtn = if _.isString(@uploadImageBtn) then $(@uploadImageBtn) else @uploadImageBtn
+    @modalCropImg = if _.isString(@modalCropImg) then $(@modalCropImg) else @modalCropImg
+
+    @cropActionsBtn = @modalCrop.find('[data-method]')
+
+    @bindEvents()
+
+  getElement: (selector) ->
+    $(selector, @form)
+
+  bindEvents: ->
+    _this = @
+    @fileInput.on 'change', (e) ->
+      _this.onFileInputChange(e, @)
+
+    @pickImageEl.on 'click', @onPickImageClick
+    @modalCrop.on 'shown.bs.modal', @onModalShow
+    @modalCrop.on 'hidden.bs.modal', @onModalHide
+    @uploadImageBtn.on 'click', @onUploadImageBtnClick
+    @cropActionsBtn.on 'click', (e) ->
+      btn = @
+      _this.onActionBtnClick(btn)
+    @croppedImageBlob = null
+
+  onPickImageClick: =>
+    @fileInput.trigger('click')
+
+  onModalShow: =>
+    _this = @
+    @modalCropImg.cropper(
+      viewMode: 1
+      center: false
+      aspectRatio: 1
+      modal: true
+      scalable: false
+      rotatable: false
+      zoomable: true
+      dragMode: 'move'
+      guides: false
+      zoomOnTouch: false
+      zoomOnWheel: false
+      cropBoxMovable: false
+      cropBoxResizable: false
+      toggleDragModeOnDblclick: false
+      built: ->
+        $image = $(@)
+        container = $image.cropper 'getContainerData'
+        cropBoxWidth = _this.cropBoxWidth;
+        cropBoxHeight = _this.cropBoxHeight;
+
+        $image.cropper('setCropBoxData',
+          width: cropBoxWidth,
+          height: cropBoxHeight,
+          left: (container.width - cropBoxWidth) / 2,
+          top: (container.height - cropBoxHeight) / 2
+        )
+    )
+
+
+  onModalHide: =>
+    @modalCropImg
+      .attr('src', '') # Remove attached image
+      .cropper('destroy') # Destroy cropper instance
+
+  onUploadImageBtnClick: (e) =>
+    e.preventDefault()
+    @setBlob()
+    @setPreview()
+    @modalCrop.modal('hide')
+    @fileInput.val('')
+
+  onActionBtnClick: (btn) ->
+    data = $(btn).data()
+
+    if @modalCropImg.data('cropper') && data.method
+      result = @modalCropImg.cropper data.method, data.option
+
+  onFileInputChange: (e, input) ->
+    @readFile(input)
+
+  readFile: (input) ->
+    _this = @
+    reader = new FileReader
+    reader.onload = ->
+      _this.modalCropImg.attr('src', reader.result)
+      _this.modalCrop.modal('show')
+
+    reader.readAsDataURL(input.files[0])
+
+  dataURLtoBlob: (dataURL) ->
+    binary = atob(dataURL.split(',')[1])
+    array = []
+    for v, k in  binary
+      array.push(binary.charCodeAt(k))
+    new Blob([new Uint8Array(array)], type: 'image/png')
+
+  setPreview: ->
+    @previewImage.attr('src', @dataURL)
+    filename = @fileInput.val().replace(FILENAMEREGEX, '')
+    @filename.text(filename)
+
+  setBlob: ->
+    @dataURL = @modalCropImg.cropper('getCroppedCanvas',
+        width: 200
+        height: 200
+      ).toDataURL('image/png')
+    @croppedImageBlob = @dataURLtoBlob(@dataURL)
+
+  getBlob: ->
+    @croppedImageBlob
+
+$.fn.glCrop = (opts) ->
+  return @.each ->
+    $(@).data('glcrop', new GitLabCrop(@, opts))
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index 960585245d797536149bb4e575903f734fc1bc32..4b78bcde77488edd0c1c8ff375ff4afbf75fc151 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -1,13 +1,29 @@
 class GitLabDropdownFilter
   BLUR_KEYCODES = [27, 40]
+  HAS_VALUE_CLASS = "has-value"
 
-  constructor: (@dropdown, @options) ->
-    @input = @dropdown.find(".dropdown-input .dropdown-input-field")
+  constructor: (@input, @options) ->
+    $inputContainer = @input.parent()
+    $clearButton = $inputContainer.find('.js-dropdown-input-clear')
+
+    # Clear click
+    $clearButton.on 'click', (e) =>
+      e.preventDefault()
+      e.stopPropagation()
+      @input
+        .val('')
+        .trigger('keyup')
+        .focus()
 
     # Key events
     timeout = ""
     @input.on "keyup", (e) =>
-      if e.keyCode is 13 && @input.val() isnt ""
+      if @input.val() isnt "" and !$inputContainer.hasClass HAS_VALUE_CLASS
+        $inputContainer.addClass HAS_VALUE_CLASS
+      else if @input.val() is "" and $inputContainer.hasClass HAS_VALUE_CLASS
+        $inputContainer.removeClass HAS_VALUE_CLASS
+
+      if e.keyCode is 13 and @input.val() isnt ""
         if @options.enterCallback
           @options.enterCallback()
         return
@@ -95,7 +111,9 @@ class GitLabDropdown
 
     # Init filiterable
     if @options.filterable
-      @filter = new GitLabDropdownFilter @dropdown,
+      @input = @dropdown.find('.dropdown-input .dropdown-input-field')
+
+      @filter = new GitLabDropdownFilter @input,
         remote: @options.filterRemote
         query: @options.data
         keys: @options.search.fields
@@ -103,6 +121,7 @@ class GitLabDropdown
           return @fullData
         callback: (data) =>
           @parseData data
+          @highlightRow 1
         enterCallback: =>
           @selectFirstRow()
 
@@ -224,11 +243,19 @@ class GitLabDropdown
 
   noResults: ->
     html = "<li>"
-    html += "<a href='#' class='is-focused'>"
+    html += "<a href='#' class='dropdown-menu-empty-link is-focused'>"
     html += "No matching results."
     html += "</a>"
     html += "</li>"
 
+  highlightRow: (index) ->
+    if @input.val() isnt ""
+      selector = '.dropdown-content li:first-child a'
+      if @dropdown.find(".dropdown-toggle-page").length
+        selector = ".dropdown-page-one .dropdown-content li:first-child a"
+
+      $(selector).addClass 'is-focused'
+
   rowClicked: (el) ->
     fieldName = @options.fieldName
     field = @dropdown.parent().find("input[name='#{fieldName}']")
@@ -272,7 +299,7 @@ class GitLabDropdown
     if @dropdown.find(".dropdown-toggle-page").length
       selector = ".dropdown-page-one .dropdown-content li:first-child a"
 
-    # similute a click on the first link
+    # simulate a click on the first link
     $(selector).trigger "click"
 
 $.fn.glDropdown = (opts) ->
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index e52b73f94f6b2308385aefd578ad7e5e071b807d..d6d09b36d8ddbe28cdafbf40b71e5fbdab958b6a 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -1,7 +1,7 @@
-#= require jquery.waitforimages
-
 class @IssuableContext
   constructor: ->
+    @initParticipants()
+
     new UsersSelect()
     $('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
 
@@ -17,3 +17,27 @@ class @IssuableContext
       block.find('.js-select2').select2("open")
 
     $(".right-sidebar").niceScroll()
+
+  initParticipants: ->
+    _this = @
+    $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
+
+    $(".js-participants-author").each (i) ->
+      if i >= _this.PARTICIPANTS_ROW_COUNT
+        $(@)
+          .addClass "js-participants-hidden"
+          .hide()
+
+  toggleHiddenParticipants: (e) ->
+    e.preventDefault()
+
+    currentText = $(this).text().trim()
+    lessText = $(this).data("less-text")
+    originalText = $(this).data("original-text")
+
+    if currentText is originalText
+      $(this).text(lessText)
+    else
+      $(this).text(originalText)
+
+    $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index f50df1f5ea3f1da8f8d07baa5bf43a702d8e612e..d663e34871c244bccefb4b01bfdab00b12a206d6 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -7,7 +7,6 @@ class @Issue
     # Prevent duplicate event bindings
     @disableTaskList()
     @fixAffixScroll()
-    @initParticipants()
     if $('a.btn-close').length
       @initTaskList()
       @initIssueBtnEventListeners()
@@ -85,27 +84,3 @@ class @Issue
       type: 'PATCH'
       url: $('form.js-issuable-update').attr('action')
       data: patchData
-
-  initParticipants: ->
-    _this = @
-    $(document).on "click", ".js-participants-more", @toggleHiddenParticipants
-
-    $(".js-participants-author").each (i) ->
-      if i >= _this.PARTICIPANTS_ROW_COUNT
-        $(@)
-          .addClass "js-participants-hidden"
-          .hide()
-
-  toggleHiddenParticipants: (e) ->
-    e.preventDefault()
-
-    currentText = $(this).text().trim()
-    lessText = $(this).data("less-text")
-    originalText = $(this).data("original-text")
-
-    if currentText is originalText
-      $(this).text(lessText)
-    else
-      $(this).text(originalText)
-
-    $(".js-participants-hidden").toggle()
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index 4a0c18a99a6bce7a24791f8fd451abe85f9fe510..e08648d583b169af9993de3c80da4284d2a46354 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -6,7 +6,7 @@ class @LabelsSelect
       labelUrl = $dropdown.data('labels')
       selectedLabel = $dropdown.data('selected')
       if selectedLabel
-        selectedLabel = selectedLabel.split(',')
+        selectedLabel = selectedLabel.toString().split(',')
       newLabelField = $('#new_label_name')
       newColorField = $('#new_label_color')
       showNo = $dropdown.data('show-no')
@@ -14,28 +14,81 @@ class @LabelsSelect
       defaultLabel = $dropdown.data('default-label')
 
       if newLabelField.length
+        $newLabelCreateButton = $('.js-new-label-btn')
+        $colorPreview = $('.js-dropdown-label-color-preview')
+        $newLabelError = $dropdown.parent().find('.js-label-error')
+        $newLabelError.hide()
+
+        # Suggested colors in the dropdown to chose from pre-chosen colors
         $('.suggest-colors-dropdown a').on 'click', (e) ->
           e.preventDefault()
           e.stopPropagation()
-          newColorField.val $(this).data('color')
-          $('.js-dropdown-label-color-preview')
+          newColorField
+            .val($(this).data('color'))
+            .trigger('change')
+          $colorPreview
             .css 'background-color', $(this).data('color')
+            .parent()
             .addClass 'is-active'
 
-        $('.js-new-label-btn').on 'click', (e) ->
+        # Cancel button takes back to first page
+        resetForm = ->
+          newLabelField
+            .val ''
+            .trigger 'change'
+          newColorField
+            .val ''
+            .trigger 'change'
+          $colorPreview
+            .css 'background-color', ''
+            .parent()
+            .removeClass 'is-active'
+
+        $('.dropdown-menu-back').on 'click', ->
+          resetForm()
+
+        $('.js-cancel-label-btn').on 'click', (e) ->
           e.preventDefault()
           e.stopPropagation()
+          resetForm()
+          $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
 
+        # Listen for change and keyup events on label and color field
+        # This allows us to enable the button when ready
+        enableLabelCreateButton = ->
           if newLabelField.val() isnt '' and newColorField.val() isnt ''
-            $('.js-new-label-btn').disable()
+            $newLabelCreateButton.enable()
+          else
+            $newLabelCreateButton.disable()
+
+        newLabelField.on 'keyup change', enableLabelCreateButton
 
-            # Create new label with API
-            Api.newLabel projectId, {
-              name: newLabelField.val()
-              color: newColorField.val()
-            }, (label) ->
-              $('.js-new-label-btn').enable()
-              $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
+        newColorField.on 'keyup change', enableLabelCreateButton
+
+        # Send the API call to create the label
+        $newLabelCreateButton
+          .disable()
+          .on 'click', (e) ->
+            e.preventDefault()
+            e.stopPropagation()
+
+            if newLabelField.val() isnt '' and newColorField.val() isnt ''
+              $newLabelError.hide()
+              $('.js-new-label-btn').disable()
+
+              # Create new label with API
+              Api.newLabel projectId, {
+                name: newLabelField.val()
+                color: newColorField.val()
+              }, (label) ->
+                $('.js-new-label-btn').enable()
+
+                if label.message?
+                  $newLabelError
+                    .text label.message
+                    .show()
+                else
+                  $('.dropdown-menu-back', $dropdown.parent()).trigger 'click'
 
       $dropdown.glDropdown(
         data: (term, callback) ->
@@ -68,8 +121,11 @@ class @LabelsSelect
           else
             selected = if label.title is selectedLabel then 'is-active' else ''
 
+          color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
+
           "<li>
             <a href='#' class='#{selected}'>
+              #{color}
               #{label.title}
             </a>
           </li>"
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index 20f87440551ad12629ba861f169eb2fb3be0618b..ae87c6c4e40bba1dc50e6e1bba288e7504fac0fd 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -1,5 +1,9 @@
 class @Profile
-  constructor: ->
+  constructor: (opts = {}) ->
+    {
+      @form = $('.edit-user')
+    } = opts
+
     # Automatically submit the Preferences form when any of its radio buttons change
     $('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
       $(this).parents('form').submit()
@@ -17,14 +21,46 @@ class @Profile
     $('.update-notifications').on 'ajax:complete', ->
       $(this).find('.btn-save').enable()
 
-    $('.js-choose-user-avatar-button').bind "click", ->
-      form = $(this).closest("form")
-      form.find(".js-user-avatar-input").click()
+    @bindEvents()
+
+    cropOpts =
+      filename: '.js-avatar-filename'
+      previewImage: '.avatar-image .avatar'
+      modalCrop: '.modal-profile-crop'
+      pickImageEl: '.js-choose-user-avatar-button'
+      uploadImageBtn: '.js-upload-user-avatar'
+      modalCropImg: '.modal-profile-crop-image'
+
+    @avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data 'glcrop'
+
+  bindEvents: ->
+    @form.on 'submit', @onSubmitForm
+
+  onSubmitForm: (e) =>
+    e.preventDefault()
+    @saveForm()
+
+  saveForm: ->
+    self = @
+
+    formData = new FormData(@form[0])
+    formData.append('user[avatar]', @avatarGlCrop.getBlob(), 'avatar.png')
 
-    $('.js-user-avatar-input').bind "change", ->
-      form = $(this).closest("form")
-      filename = $(this).val().replace(/^.*[\\\/]/, '')
-      form.find(".js-avatar-filename").text(filename)
+    $.ajax
+      url: @form.attr('action')
+      type: @form.attr('method')
+      data: formData
+      dataType: "json"
+      processData: false
+      contentType: false
+      success: (response) ->
+        new Flash(response.message, 'notice')
+      error: (jqXHR) ->
+        new Flash(jqXHR.responseJSON.message, 'alert')
+      complete: ->
+        window.scrollTo 0, 0
+        # Enable submit button after requests ends
+        self.form.find(':input[disabled]').enable()
 
 $ ->
   # Extract the SSH Key title from its comment
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index eea3f5ee910425449d453b44783e7543f2076459..860d4f438d0117b8efdca46a7725927663f4c7ee 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -4,7 +4,6 @@ expanded = 'page-sidebar-expanded'
 toggleSidebar = ->
   $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
   $('header').toggleClass("header-collapsed header-expanded")
-  $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
   $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
   $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
 
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 3d6452d2f4693d36691df7ff987bc257de83d761..84193400890b4f8a19f6fe67146c2101bf68b08c 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -30,6 +30,7 @@ class @UsersSelect
               if showNullUser
                 showDivider += 1
                 users.unshift(
+                  beforeDivider: true
                   name: 'Unassigned',
                   id: 0
                 )
@@ -39,6 +40,7 @@ class @UsersSelect
                 name = showAnyUser
                 name = 'Any User' if name == true
                 anyUser = {
+                  beforeDivider: true
                   name: name,
                   id: null
                 }
@@ -75,20 +77,27 @@ class @UsersSelect
           selected = if user.id is selectedId then "is-active" else ""
           img = ""
 
-          if avatar
-            img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
-
-          "<li>
-            <a href='#' class='dropdown-menu-user-link #{selected}'>
-              #{img}
-              <strong class='dropdown-menu-user-full-name'>
+          if user.beforeDivider?
+            "<li>
+              <a href='#' class='#{selected}'>
                 #{user.name}
-              </strong>
-              <span class='dropdown-menu-user-username'>
-                #{username}
-              </span>
-            </a>
-          </li>"
+              </a>
+            </li>"
+          else
+            if avatar
+              img = "<img src='#{avatar}' class='avatar avatar-inline' width='30' />"
+
+            "<li>
+              <a href='#' class='dropdown-menu-user-link #{selected}'>
+                #{img}
+                <strong class='dropdown-menu-user-full-name'>
+                  #{user.name}
+                </strong>
+                <span class='dropdown-menu-user-username'>
+                  #{username}
+                </span>
+              </a>
+            </li>"
       )
 
     $('.ajax-users-select').each (i, select) =>
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 2d301d21ab95eabd8042f1f2ce8d4a8e33198a22..e2d590f4df4e8bd37ffeb1952adc6f7d2f4b0f0e 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -9,6 +9,7 @@
  *= require_self
  *= require dropzone/basic
  *= require cal-heatmap
+ *= require cropper.css
 */
 
 /*
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index bc03c2180becdd93fd2aa80311f88ef2d009107f..9b676d759e050045b4f4416ecb1e043e12e59d05 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -292,8 +292,11 @@ table {
 }
 
 .btn-sign-in {
-  margin-top: 10px;
   text-shadow: none;
+
+  @media (min-width: $screen-sm-min) {
+    margin-top: 11px;
+  }
 }
 
 .side-filters {
@@ -375,7 +378,7 @@ table {
     position: absolute;
     top: 0;
     right: 0;
-    width: 250px !important;
+    min-width: 250px;
     visibility: hidden;
   }
 }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d92cf6e6c44f7ca4c1eeaec30e4c01e632a20c38..2d616fc660c520afb058abe39e37f8df17e7fd8f 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -130,6 +130,12 @@
       text-decoration: none;
       outline: 0;
     }
+
+    &.dropdown-menu-empty-link {
+      &.is-focused {
+        background-color: $dropdown-empty-row-bg;
+      }
+    }
   }
 }
 
@@ -183,7 +189,7 @@
 }
 
 .dropdown-select {
-  width: 280px;
+  width: 300px;
 }
 
 .dropdown-menu-align-right {
@@ -237,7 +243,7 @@
 
 .dropdown-title-button {
   position: absolute;
-  top: -1px;
+  top: 0;
   padding: 0;
   color: $dropdown-title-btn-color;
   font-size: 14px;
@@ -270,6 +276,22 @@
     font-size: 12px;
     pointer-events: none;
   }
+
+  .dropdown-input-clear {
+    display: none;
+    cursor: pointer;
+    pointer-events: all;
+  }
+
+  &.has-value {
+    .dropdown-input-clear {
+      display: block;
+    }
+
+    .dropdown-input-search {
+      display: none;
+    }
+  }
 }
 
 .dropdown-input-field {
@@ -286,13 +308,13 @@
     border-color: $dropdown-input-focus-border;
     box-shadow: 0 0 4px $dropdown-input-focus-shadow;
 
-    + .fa {
+    ~ .fa {
       color: $dropdown-link-color;
     }
   }
 
   &:hover {
-    + .fa {
+    ~ .fa {
       color: $dropdown-link-color;
     }
   }
@@ -338,11 +360,12 @@
   }
 }
 
-.dropdown-menu-labels {
-  .label {
-    position: relative;
-    width: 30px;
-    margin-right: 5px;
-    text-indent: -99999px;
-  }
+.dropdown-label-box {
+  position: relative;
+  top: 3px;
+  margin-right: 5px;
+  display: inline-block;
+  width: 15px;
+  height: 15px;
+  border-radius: $border-radius-base;
 }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 646e261083121b342c4ba934fa05a34632a7c3a6..ad0e88cda86fe8d1ca231488ef87ee0aa46bc02a 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -50,6 +50,10 @@
       }
     }
 
+    a {
+      color: $gl-dark-link-color;
+    }
+
     .left-options {
       margin-top: -3px;
     }
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 40a508c1ebc73fa96137f1e510e8f6026fc97740..b05c5df1bd853600693dabc38a342c9927ff70d8 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -11,3 +11,11 @@
     }
   }
 }
+
+@media (max-width: $screen-xs-max) {
+  .filter-item {
+    display: block;
+    margin: 0 0 10px;
+  }
+}
+
diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
index 7a946109e3af43f990ba210db98e5cfb814036af..5f9685bc71a05cbf32e78f98d9a288264455686c 100644
--- a/app/assets/stylesheets/framework/fonts.scss
+++ b/app/assets/stylesheets/framework/fonts.scss
@@ -1,3 +1,7 @@
+// Disabling "SpaceAfterPropertyColon" linter because the linter doesn't like
+// the way the `src` property is formatted in this file.
+// scss-lint:disable SpaceAfterPropertyColon
+
 /* latin-ext */
 @font-face {
   font-family: 'Source Sans Pro';
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 71a7ecab8efb35241b2c6bfb45a2488c8fe298ac..6a68bb5c1159ccaa612fcec3511b9c89ffb796c9 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -70,6 +70,11 @@ header {
 
   .header-content {
     height: $header-height;
+    padding-right: 20px;
+
+    @media (min-width: $screen-sm-min) {
+      padding-right: 0;
+    }
 
     .title {
       margin: 0;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 5f4ce87b085e9fd0d51aa5364f93458d681c9616..95bdd6d1ea3a47325e6f84f20f1a0f1ca6c00a6a 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -102,6 +102,10 @@
       display: inline-block;
     }
 
+    .icon-label {
+      display: none;
+    }
+
     input {
       height: 34px;
       display: inline-block;
@@ -124,9 +128,38 @@
       }
     }
 
-    /* Hide on extra small devices (phones) */
     @media (max-width: $screen-xs-max) {
-      display: none;
+      padding-bottom: 0;
+
+      .btn, form, .dropdown, .dropdown-menu-toggle, .form-control {
+        margin: 0 0 10px;
+        display: block;
+        width: 100%;
+      }
+
+      form {
+        display: block;
+        height: auto;
+
+        input {
+          width: 100%;
+          margin: 0 0 10px;
+        }
+      }
+
+      .input-short {
+        width: 100%;
+      }
+
+      .icon-label {
+        display: inline-block;
+      }
+
+      // Applies on /dashboard/issues
+      .project-item-select-holder {
+        display: block;
+        margin: 0;
+      }
     }
 
     /* Small devices (tablets, 768px and lower) */
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index fa7944cdabe905eeccf43c2556724bff1537d163..e82d052f45a5aa490b638d074ad9c5e111c83d82 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -44,6 +44,7 @@
   @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
   @include border-radius ($border-radius-default);
   border: none;
+  min-width: 175px;
 }
 
 .select2-results .select2-result-label {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index be05db58c40d030d202bac790866c5477291f584..9d188317783d9171ffcbc29ab8cc4081f578b87f 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,3 +1,10 @@
+#logo {
+  z-index: 2;
+  position: absolute;
+  width: 58px;
+  cursor: pointer;
+}
+
 .page-with-sidebar {
   padding-top: $header-height;
   transition-duration: .3s;
@@ -18,28 +25,10 @@
     position: absolute;
     left: 0;
   }
-
-  #logo {
-    z-index: 2;
-    position: absolute;
-    width: 58px;
-    cursor: pointer;
-  }
-
-  &.right-sidebar-expanded {
-    /* Extra small devices (phones, less than 768px) */
-    /* No media query since this is the default in Bootstrap */
-    padding-right: 0;
-    /* Small devices (tablets, 768px and up) */
-    @media (min-width: $screen-sm-min) {
-      padding-right: $gutter_width;
-    }
-
-  }
 }
 
 .sidebar-wrapper {
-  z-index: 999;
+  z-index: 1000;
   background: $background-color;
 }
 
@@ -202,53 +191,27 @@
   }
 }
 
-@mixin expanded-sidebar {
-  padding-left: $sidebar_collapsed_width;
-
-  @media (min-width: $screen-md-min) {
-    padding-left: $sidebar_width;
-  }
-
-  &.right-sidebar-collapsed {
-    /* Extra small devices (phones, less than 768px) */
-    padding-right: 0;
-    /* Small devices (tablets, 768px and up) */
-    @media (min-width: $screen-sm-min) {
-      padding-right: $sidebar_collapsed_width;
-    }
-  }
-
-  .sidebar-wrapper {
-    width: $sidebar_width;
-
-    .nav-sidebar {
-      width: $sidebar_width;
-    }
-
-    .nav-sidebar li a{
-      width: 230px;
+.collapse-nav a {
+  width: $sidebar_width;
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  font-size: 13px;
+  background: transparent;
+  height: 40px;
+  text-align: center;
+  line-height: 40px;
+  transition-duration: .3s;
+  outline: none;
 
-      &.back-link {
-        i {
-          opacity: 0;
-        }
-      }
-    }
+  &:hover {
+    text-decoration: none;
   }
 }
 
-@mixin collapsed-sidebar {
+.page-sidebar-collapsed {
   padding-left: $sidebar_collapsed_width;
 
-  &.right-sidebar-collapsed {
-    /* Extra small devices (phones, less than 768px) */
-    padding-right: 0;
-    /* Small devices (tablets, 768px and up) */
-    @media (min-width: $screen-sm-min) {
-      padding-right: $sidebar_collapsed_width;
-    }
-  }
-
   .sidebar-wrapper {
     width: $sidebar_collapsed_width;
 
@@ -293,35 +256,48 @@
   }
 }
 
-.collapse-nav a {
-  width: $sidebar_width;
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  font-size: 13px;
-  background: transparent;
-  height: 40px;
-  text-align: center;
-  line-height: 40px;
-  transition-duration: .3s;
-  outline: none;
-}
+.page-sidebar-expanded {
+  padding-left: $sidebar_collapsed_width;
+
+  @media (min-width: $screen-md-min) {
+    padding-left: $sidebar_width;
+  }
+
+  .sidebar-wrapper {
+    width: $sidebar_width;
+
+    .nav-sidebar {
+      width: $sidebar_width;
+    }
+
+    .nav-sidebar li a {
+      width: 230px;
 
-.collapse-nav a:hover {
-  text-decoration: none;
-  background: #f2f6f7;
+      &.back-link {
+        i {
+          opacity: 0;
+        }
+      }
+    }
+  }
 }
 
-.page-sidebar-collapsed {
-  /* Extra small devices (phones, less than 768px) */
-  @include collapsed-sidebar;
+.right-sidebar-collapsed {
   padding-right: 0;
-  /* Small devices (tablets, 768px and up) */
+
   @media (min-width: $screen-sm-min) {
-    @include collapsed-sidebar;
+    padding-right: $sidebar_collapsed_width;
   }
 }
 
-.page-sidebar-expanded {
-  @include expanded-sidebar;
+.right-sidebar-expanded {
+  padding-right: 0;
+
+  @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+    padding-right: $sidebar_collapsed_width;
+  }
+
+  @media (min-width: $screen-md-min) {
+    padding-right: $gutter_width;
+  }
 }
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index be626678bd7a88e3682007c8fda9e66b9848c5a9..61e0dd4d672b1f6e95704f68df796ea6a3a6e784 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -168,13 +168,14 @@ $regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif
 */
 $dropdown-bg: #fff;
 $dropdown-link-color: #555;
-$dropdown-link-hover-bg: rgba(#000, .04);
+$dropdown-link-hover-bg: $row-hover;
+$dropdown-empty-row-bg: rgba(#000, .04);
 $dropdown-border-color: rgba(#000, .1);
 $dropdown-shadow-color: rgba(#000, .1);
 $dropdown-divider-color: rgba(#000, .1);
 $dropdown-header-color: #959494;
 $dropdown-title-btn-color: #bfbfbf;
-$dropdown-input-color: #c7c7c7;
+$dropdown-input-color: #555;
 $dropdown-input-focus-border: rgb(58, 171, 240);
 $dropdown-input-focus-shadow: rgba(#000, .2);
 $dropdown-loading-bg: rgba(#fff, .6);
diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss
index f1d42f80f56357dd708a3e173c5f0b4b827bc48e..0a13a7e0b546fc0dbccfe304d9404f5f827618d8 100644
--- a/app/assets/stylesheets/notify.scss
+++ b/app/assets/stylesheets/notify.scss
@@ -3,12 +3,12 @@ img {
   height: auto;
 }
 p.details {
-  font-style:italic;
-  color:#777
+  font-style: italic;
+  color: #777
 }
 .footer p {
-  font-size:small;
-  color:#777
+  font-size: small;
+  color: #777
 }
 pre.commit-message {
   white-space: pre-wrap;
@@ -20,5 +20,5 @@ pre.commit-message {
   color: #090;
 }
 .file-stats .deleted-file {
-  color: #B00;
+  color: #b00;
 }
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 33b3c7558ed79598a5250e3dbfcf5594e394df24..5e91496679a1b547a105a73b9e2eb44c45157564 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -99,6 +99,10 @@ li.commit {
       color: $gl-gray;
     }
 
+    .avatar {
+      margin-right: 8px;
+    }
+
     .committed_ago {
       display: inline-block;
     }
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 61ee34b695e5a64f8e23fcbf399aabd096c88e21..4e02ec4e89164475eea0bd8f50fcb0ce866a33c6 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -9,28 +9,45 @@
   }
 
   &.suggest-colors-dropdown {
-    margin-bottom: 5px;
+    margin-top: 10px;
+    margin-bottom: 10px;
+    border-radius: $border-radius-base;
+    overflow: hidden;
 
     a {
       @include border-radius(0);
-      width: 36.7px;
+      width: (100% / 7);
       margin-right: 0;
       margin-bottom: -5px;
     }
   }
 }
 
-.dropdown-label-color-preview {
-  display: none;
-  margin-top: 5px;
-  width: 100%;
-  height: 25px;
+.dropdown-new-label {
+  .dropdown-content {
+    max-height: 260px;
+  }
+}
+
+.dropdown-label-color-input {
+  position: relative;
+  margin-bottom: 10px;
 
   &.is-active {
-    display: block;
+    padding-left: 32px;
   }
 }
 
+.dropdown-label-color-preview {
+  position: absolute;
+  left: 0;
+  top: 0;
+  width: 32px;
+  height: 32px;
+  border-top-left-radius: $border-radius-base;
+  border-bottom-left-radius: $border-radius-base;
+}
+
 .label-row {
   .label {
     padding: 9px;
@@ -45,3 +62,10 @@
 .label-subscription {
   display: inline-block;
 }
+
+.dropdown-labels-error {
+  padding: 5px 10px;
+  margin-bottom: 10px;
+  background-color: $gl-danger;
+  color: $white-light;
+}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index e96dfc8e8d89eb2ca056af04154fad2546deec1b..a9656e5cae748d364e05aad92ae8066c4a232245 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -197,3 +197,24 @@
     width: 105px;
   }
 }
+
+.modal-profile-crop {
+  .modal-dialog {
+    width: 380px;
+
+    @media (max-width: $screen-sm-min) {
+      width: auto;
+    }
+
+  }
+
+  .profile-crop-image-container {
+    height: 300px;
+    margin: 0 auto;
+  }
+
+  .crop-controls {
+    padding: 10px 0 0;
+    text-align: center;
+  }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index c68bd673a6751e7122e4babfa6f6e18b3186f87f..71bde1174eeaf62d2e6f2651ff22ea6612bae9cb 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -229,6 +229,10 @@
     padding: 0 3px;
     color: #999;
   }
+
+  a {
+    color: $gl-dark-link-color;
+  }
 }
 
 .last-push-widget {
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 6f777d11641883d45b7a247bcd6ae8c7c9986e64..5e5e38a0ba6515e043acab20bbebad94d68e8924 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -1,54 +1,58 @@
-.ci-status {
-  padding: 2px 7px;
-  margin-right: 5px;
-  border: 1px solid #eee;
-  white-space: nowrap;
-  @include border-radius(4px);
+.container-fluid .content {
+  .ci-status {
+    padding: 2px 7px;
+    margin-right: 5px;
+    border: 1px solid #eee;
+    white-space: nowrap;
+    @include border-radius(4px);
 
-  &:hover {
-    text-decoration: none;
-  }
+    &:hover {
+      text-decoration: none;
+    }
 
-  &.ci-failed {
-    color: $gl-danger;
-    border-color: $gl-danger;
-  }
+    &.ci-failed {
+      color: $gl-danger;
+      border-color: $gl-danger;
+    }
 
-  &.ci-success {
-    color: $gl-success;
-    border-color: $gl-success;
-  }
+    &.ci-success {
+      color: $gl-success;
+      border-color: $gl-success;
+    }
 
-  &.ci-info {
-    color: $gl-info;
-    border-color: $gl-info;
-  }
+    &.ci-info {
+      color: $gl-info;
+      border-color: $gl-info;
+    }
 
-  &.ci-disabled {
-    color: $gl-gray;
-    border-color: $gl-gray;
+    &.ci-canceled,
+    &.ci-skipped,
+    &.ci-disabled {
+      color: $gl-gray;
+      border-color: $gl-gray;
+    }
+
+    &.ci-pending,
+    &.ci-running {
+      color: $gl-warning;
+      border-color: $gl-warning;
+    }
   }
 
-  &.ci-pending,
-  &.ci-running {
+  .ci-status-icon-success {
+    color: $gl-success;
+  }
+  .ci-status-icon-failed {
+    color: $gl-danger;
+  }
+  .ci-status-icon-running,
+  .ci-status-icon-pending {
     color: $gl-warning;
-    border-color: $gl-warning;
   }
-}
-
-.ci-status-icon-success {
-  @extend .cgreen;
-}
-.ci-status-icon-failed {
-  @extend .cred;
-}
-.ci-status-icon-running,
-.ci-status-icon-pending {
-  // These are standard text color
-}
-.ci-status-icon-canceled,
-.ci-status-icon-disabled,
-.ci-status-icon-not-found,
-.ci-status-icon-skipped {
-  @extend .cgray;
+  .ci-status-icon-canceled,
+  .ci-status-icon-disabled,
+  .ci-status-icon-not-found,
+  .ci-status-icon-skipped {
+    color: $gl-gray;
+  }
 }
diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb
index 54ea1e454fc6fa17651a8207ba7c842e58fad297..5c503c5b69896c4e9d92477ed1817307bc9c94b7 100644
--- a/app/controllers/concerns/global_milestones.rb
+++ b/app/controllers/concerns/global_milestones.rb
@@ -6,7 +6,6 @@ module GlobalMilestones
     @milestones = MilestonesFinder.new.execute(@projects, params)
     @milestones = GlobalMilestone.build_collection(@milestones)
     @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
-    @milestones = Kaminari.paginate_array(@milestones).page(params[:page])
   end
 
   def milestone
diff --git a/app/controllers/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb
index 962ea38d6c9c01020e2cc888a1b915261032c5af..9d3d1c23c28c9784676322441472c602cf9d5863 100644
--- a/app/controllers/dashboard/application_controller.rb
+++ b/app/controllers/dashboard/application_controller.rb
@@ -1,3 +1,9 @@
 class Dashboard::ApplicationController < ApplicationController
   layout 'dashboard'
+
+  private
+
+  def projects
+    @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
+  end
 end
diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..23a4ef21ea20e185b5b334025cb25ddef9598f50
--- /dev/null
+++ b/app/controllers/dashboard/labels_controller.rb
@@ -0,0 +1,9 @@
+class Dashboard::LabelsController < Dashboard::ApplicationController
+  def index
+    labels = Label.where(project_id: projects).select(:title, :color).uniq(:title)
+
+    respond_to do |format|
+      format.json { render json: labels }
+    end
+  end
+end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 2bdce0f8a0025326c4e6742931fb262b1878cc63..fa9c6c054f0fbb3da50aa224cf42aa345d30c9d7 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -2,18 +2,19 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController
   include GlobalMilestones
 
   before_action :projects
-  before_action :milestones, only: [:index]
   before_action :milestone, only: [:show]
 
   def index
+    respond_to do |format|
+      format.html do
+        @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+      end
+      format.json do
+        render json: milestones
+      end
+    end
   end
 
   def show
   end
-
-  private
-
-  def projects
-    @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
-  end
 end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index b538c7d160822a270794ce954889f92b16d19ae2..1dce4a21729f88fea4b5d2cee06c933c63fc5e8a 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -3,7 +3,7 @@ class DashboardController < Dashboard::ApplicationController
   include MergeRequestsAction
 
   before_action :event_filter, only: :activity
-  before_action :projects, only: [:issues, :merge_requests, :labels, :milestones]
+  before_action :projects, only: [:issues, :merge_requests]
 
   respond_to :html
 
@@ -20,29 +20,6 @@ class DashboardController < Dashboard::ApplicationController
     end
   end
 
-  def labels
-    labels = Label.where(project_id: @projects).select(:title, :color).uniq(:title)
-
-    respond_to do |format|
-      format.json do
-        render json: labels
-      end
-    end
-  end
-
-  def milestones
-    milestones = Milestone.where(project_id: @projects).active
-    epoch = DateTime.parse('1970-01-01')
-    grouped_milestones = GlobalMilestone.build_collection(milestones)
-    grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
-
-    respond_to do |format|
-      format.json do
-        render json: grouped_milestones
-      end
-    end
-  end
-
   protected
 
   def load_events
@@ -57,8 +34,4 @@ class DashboardController < Dashboard::ApplicationController
     @events = @event_filter.apply_filter(@events).with_associations
     @events = @events.limit(20).offset(params[:offset] || 0)
   end
-
-  def projects
-    @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
-  end
 end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 0028f072d5b568e42a0d3fcabb883f9efebae329..b23c3022fb5dc57994be036b41d27802cb042029 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -2,11 +2,15 @@ class Groups::MilestonesController < Groups::ApplicationController
   include GlobalMilestones
 
   before_action :group_projects
-  before_action :milestones, only: [:index]
   before_action :milestone, only: [:show, :update]
   before_action :authorize_admin_milestones!, only: [:new, :create, :update]
 
   def index
+    respond_to do |format|
+      format.html do
+        @milestones = Kaminari.paginate_array(milestones).page(params[:page])
+      end
+    end
   end
 
   def new
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 9042d8e5f0d1d0107b5e03a3bc2a83c3784bad2b..c5fa756d02bb8098603cb9714395a1e7eccf1cc7 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -11,15 +11,16 @@ class ProfilesController < Profiles::ApplicationController
   def update
     user_params.except!(:email) if @user.ldap_user?
 
-    if @user.update_attributes(user_params)
-      flash[:notice] = "Profile was successfully updated"
-    else
-      messages = @user.errors.full_messages.uniq.join('. ')
-      flash[:alert] = "Failed to update profile. #{messages}"
-    end
-
     respond_to do |format|
-      format.html { redirect_back_or_default(default: { action: 'show' }) }
+      if @user.update_attributes(user_params)
+        message = "Profile was successfully updated"
+        format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
+        format.json { render json: { message: message } }
+      else
+        message = @user.errors.full_messages.uniq.join('. ')
+        format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: "Failed to update profile. #{message}" }) }
+        format.json { render json: { message: message }, status: :unprocessable_entity }
+      end
     end
   end
 
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index ad04c646e1b031fe77083f3e725befdc1b536aff..627be74a38fd647f31951780610d35cffd3f178e 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -26,6 +26,10 @@ class RootController < Dashboard::ProjectsController
       redirect_to activity_dashboard_path
     when 'starred_project_activity'
       redirect_to activity_dashboard_path(filter: 'starred')
+    when 'groups'
+      redirect_to dashboard_groups_path
+    when 'todos'
+      redirect_to dashboard_todos_path
     else
       return
     end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index ceff1fbb1618a0d16b1f658a438eabc4e0fc4da5..316a10b7da36ee52b6b349d13d9cce4505670b0d 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -70,7 +70,8 @@ module DropdownsHelper
   def dropdown_filter(placeholder)
     content_tag :div, class: "dropdown-input" do
       filter_output = search_field_tag nil, nil, class: "dropdown-input-field", placeholder: placeholder
-      filter_output << icon('search')
+      filter_output << icon('search', class: "dropdown-input-search")
+      filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
 
       filter_output.html_safe
     end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index a67a6b208e256bdfb9aa4023569e57b16b071ed4..d3e5e3aa8b9b69eb2bd19153fd860b3eeeb47f85 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -194,7 +194,7 @@ module EventsHelper
   end
 
   def event_to_atom(xml, event)
-    if event.proper?(current_user)
+    if event.visible_to_user?(current_user)
       xml.entry do
         event_link = event_feed_url(event)
         event_title = event_feed_title(event)
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index e0a8552dfa757c37c10d1c150b57855d5986f3fc..3dded7c2f231071ae6995c8c157512d27564652b 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -114,7 +114,7 @@ module LabelsHelper
     if @project
       namespace_project_labels_path(@project.namespace, @project, :json)
     else
-      labels_dashboard_path(:json)
+      dashboard_labels_path(:json)
     end
   end
 
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index c9d8787bd196c0abc5c41475767bf6ff65468760..87fc2db69016cc307f978b2eda0259639a163deb 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -50,7 +50,7 @@ module MilestonesHelper
     if @project
       namespace_project_milestones_path(@project.namespace, @project, :json)
     else
-      milestones_dashboard_path(:json)
+      dashboard_milestones_path(:json)
     end
   end
 
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index c73cb3028eebe65c980c8e7dd9225b2db6cdb20c..c3832cf5d65e0dbafe336b3d0fd97643cbb6ad11 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -12,7 +12,9 @@ module PreferencesHelper
     projects: 'Your Projects (default)',
     stars:    'Starred Projects',
     project_activity: "Your Projects' Activity",
-    starred_project_activity: "Starred Projects' Activity"
+    starred_project_activity: "Starred Projects' Activity",
+    groups: "Your Groups",
+    todos: "Your Todos"
   }.with_indifferent_access.freeze
 
   # Returns an Array usable by a select field for more user-friendly option text
diff --git a/app/models/commit.rb b/app/models/commit.rb
index ce0b85d50cf95444825ff6e822acda868a1eb8a8..d0dbe009d0d1833d0339b88e729a60a9d0ffb3d0 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -230,7 +230,7 @@ class Commit
   end
 
   def revert_message
-    %Q{Revert "#{title}"\n\n#{revert_description}}
+    %Q{Revert "#{title.strip}"\n\n#{revert_description}}
   end
 
   def reverts_commit?(commit)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 476e1ce7af0bc3141fe5fb1bf0556cbce340d5ff..cf5b2c716756f0f30d405dbd7562a91ce97a6a85 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -41,7 +41,7 @@ module Issuable
 
     scope :join_project, -> { joins(:project) }
     scope :references_project, -> { references(:project) }
-    scope :non_archived, -> { join_project.merge(Project.non_archived) }
+    scope :non_archived, -> { join_project.merge(Project.non_archived.only(:where)) }
 
     delegate :name,
              :email,
diff --git a/app/models/event.rb b/app/models/event.rb
index a5cfeaf388ea4563d4df915a35c03a1a8bec891a..12183524b79d674485276aae6c9ccf41bee18611 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -73,15 +73,15 @@ class Event < ActiveRecord::Base
     end
   end
 
-  def proper?(user = nil)
+  def visible_to_user?(user = nil)
     if push?
       true
     elsif membership_changed?
       true
     elsif created_project?
       true
-    elsif issue?
-      Ability.abilities.allowed?(user, :read_issue, issue)
+    elsif issue? || issue_note?
+      Ability.abilities.allowed?(user, :read_issue, note? ? note_target : target)
     else
       ((merge_request? || note?) && target) || milestone?
     end
@@ -298,6 +298,10 @@ class Event < ActiveRecord::Base
     target.noteable_type == "Commit"
   end
 
+  def issue_note?
+    note? && target && target.noteable_type == "Issue"
+  end
+
   def note_project_snippet?
     target.noteable_type == "Snippet"
   end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index f32db59ac9fd40e68c49286e5833aa4fcb1db4e2..ed960cb39f44beff148345dbb6f91f865c9c951e 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -146,7 +146,8 @@ class Issue < ActiveRecord::Base
       return false unless user.can?(:admin_issue, to_project)
     end
 
-    !moved? && user.can?(:admin_issue, self.project)
+    !moved? && persisted? &&
+      user.can?(:admin_issue, self.project)
   end
 
   def to_branch_name
diff --git a/app/models/label.rb b/app/models/label.rb
index f7ffc0b7f3679936b01b260aae015c96630ac884..500d5a355217b9ab39655727456825d85bc5dc8e 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -97,12 +97,12 @@ class Label < ActiveRecord::Base
     end
   end
 
-  def open_issues_count
-    issues.opened.count
+  def open_issues_count(user = nil)
+    issues.visible_to_user(user).opened.count
   end
 
-  def closed_issues_count
-    issues.closed.count
+  def closed_issues_count(user = nil)
+    issues.visible_to_user(user).closed.count
   end
 
   def open_merge_requests_count
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index de7183bf6b4e836b97700f6c816998c1afe5d771..bbd59eab9aecea74c0cb0875fe97bc0fd877c81b 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -83,7 +83,7 @@ class Milestone < ActiveRecord::Base
   end
 
   def self.upcoming
-    self.where('due_date > ?', Time.now).order(due_date: :asc).first
+    self.where('due_date > ?', Time.now).reorder(due_date: :asc).first
   end
 
   def to_reference(from_project = nil)
diff --git a/app/models/project.rb b/app/models/project.rb
index b46435632601f96b9477993a11ef09fdc1098df9..52f70256be305f3ad4aa1ae6b79ccd5b6af50d1c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -304,7 +304,7 @@ class Project < ActiveRecord::Base
     end
 
     def find_with_namespace(id)
-      namespace_path, project_path = id.split('/')
+      namespace_path, project_path = id.split('/', 2)
 
       return nil if !namespace_path || !project_path
 
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 13154eb420583c53d956975e0580ddcfa4929767..c07e807204357328e103158e0cd2e21ba7c9305b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -467,6 +467,18 @@ class Repository
     end
   end
 
+  def gitlab_ci_yml
+    return nil if !exists? || empty?
+
+    @gitlab_ci_yml ||= tree(:head).blobs.find do |file|
+      file.name == '.gitlab-ci.yml'
+    end
+  rescue Rugged::ReferenceError
+    # For unknow reason spinach scenario "Scenario: I change project path"
+    # lead to "Reference 'HEAD' not found" exception from Repository#empty?
+    nil
+  end
+
   def head_commit
     @head_commit ||= commit(self.root_ref)
   end
@@ -877,6 +889,8 @@ class Repository
   end
 
   def avatar
+    return nil unless exists?
+
     @avatar ||= cache.fetch(:avatar) do
       AVATAR_FILES.find do |file|
         blob_at_branch('master', file)
diff --git a/app/models/user.rb b/app/models/user.rb
index 9c315cfe9662e1d11ee387418dca3e0e1f4b247f..128ddc2a694ebbe80b033bdeadf9a0ac0199cca2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -184,7 +184,7 @@ class User < ActiveRecord::Base
 
   # User's Dashboard preference
   # Note: When adding an option, it MUST go on the end of the array.
-  enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity]
+  enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos]
 
   # User's Project preference
   # Note: When adding an option, it MUST go on the end of the array.
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 002f7ba12785aca3a496af1a3c9e8a4f90455526..2cd51a7610f6310e5c0c098244e74e2ddba27e0a 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -1,7 +1,7 @@
 module Ci
   class CreateBuildsService
     def execute(commit, stage, ref, tag, user, trigger_request, status)
-      builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag)
+      builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag, trigger_request)
 
       # check when to create next build
       builds_attrs = builds_attrs.select do |build_attrs|
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index 3cfbafe1576d1e375a78c0950314efb7726e81c5..a5efb21fab6868aa2c371e2f48b55dcd110660c7 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -54,7 +54,8 @@ module Issues
         new_note = note.dup
         new_params = { project: @new_project, noteable: @new_issue,
                        note: unfold_references(new_note.note),
-                       created_at: note.created_at }
+                       created_at: note.created_at,
+                       updated_at: note.updated_at }
 
         new_note.update(new_params)
       end
@@ -78,6 +79,8 @@ module Issues
     end
 
     def unfold_references(content)
+      return unless content
+
       rewriter = Gitlab::Gfm::ReferenceRewriter.new(content, @old_project,
                                                     @current_user)
       rewriter.rewrite(@new_project)
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 3bdf00a8291cf56a3c633304a8c47d7d65b41a3f..eff0d96f93dd07e0d1ec4336f952d957768b7274 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -162,6 +162,7 @@ class NotificationService
 
     recipients = add_subscribed_users(recipients, note.noteable)
     recipients = reject_unsubscribed_users(recipients, note.noteable)
+    recipients = reject_users_without_access(recipients, note.noteable)
 
     recipients.delete(note.author)
     recipients = recipients.uniq
@@ -376,6 +377,14 @@ class NotificationService
     end
   end
 
+  def reject_users_without_access(recipients, target)
+    return recipients unless target.is_a?(Issue)
+
+    recipients.select do |user|
+      user.can?(:read_issue, target)
+    end
+  end
+
   def add_subscribed_users(recipients, target)
     return recipients unless target.respond_to? :subscribers
 
@@ -464,15 +473,16 @@ class NotificationService
     end
 
     recipients = reject_unsubscribed_users(recipients, target)
+    recipients = reject_users_without_access(recipients, target)
 
     recipients.delete(current_user)
-
     recipients.uniq
   end
 
   def build_relabeled_recipients(target, current_user, labels:)
     recipients = add_labels_subscribers([], target, labels: labels)
     recipients = reject_unsubscribed_users(recipients, target)
+    recipients = reject_users_without_access(recipients, target)
     recipients.delete(current_user)
     recipients.uniq
   end
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index dfa5f80eef8bed7c6fd6477ed659600cbba1acfb..1eec4db45a07be6dce1bac2b8b3f58f28061d459 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -10,6 +10,8 @@
     - if current_user
       = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
         = icon('rss')
+        %span.icon-label
+          Subscribe
     = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
 
 = render 'shared/issuable/filter', type: :issues
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index ea0b66c932b6c027ca41ce3cd720dc6f07d59575..55f4a6f287d208f22254c71e20eb0c41fd404812 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -77,7 +77,7 @@
                       %em Authorization was granted by entering your username and password in the application.
                   %td= token.created_at
                   %td= token.scopes
-                  %td= render 'delete_form', token: token
+                  %td= render 'doorkeeper/authorized_applications/delete_form', token: token
       - else
         .profile-settings-message.text-center
           You don't have any authorized applications
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 2d9d9dd634243eb44d8878f0851c065fff0446aa..42c2764e7e2335447b0057561da4fae55900c746 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -1,4 +1,4 @@
-- if event.proper?(current_user)
+- if event.visible_to_user?(current_user)
   .event-item{class: "#{event.body? ? "event-block" : "event-inline" }"}
     .event-item-timestamp
       #{time_ago_with_tooltip(event.created_at)}
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index b0805593fdc8e43acd304842d64d26e849b5202d..aea35c50862c5d3da9fa95651aba5c814e2cbdc8 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -10,6 +10,8 @@
     - if current_user
       = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
         = icon('rss')
+        %span.icon-label
+          Subscribe
     = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
 
 = render 'shared/issuable/filter', type: :issues
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 820743dc8dd8d63e6584fca37b4ecc78d487f3cc..3d16ecb097a5735b41970a701c58590343823d8f 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -17,7 +17,7 @@
   .cover-title
     %h1
       = @group.name
-      %span.visibility-icon.has_tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+      %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
         = visibility_level_icon(@group.visibility_level, fw: false)
 
   .cover-desc.username
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index bfa5937cf3f3205209cc1ad405d20583066c65c5..0f3b8119379cf124a1f1ff617c33118a545a0936 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -6,7 +6,7 @@
         = icon('bars')
 
       .navbar-collapse.collapse
-        %ul.nav.navbar-nav.pull-right
+        %ul.nav.navbar-nav
           %li.hidden-sm.hidden-xs
             = render 'layouts/search'
           %li.visible-sm.visible-xs
@@ -38,8 +38,9 @@
               = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
                 = icon('sign-out')
           - else
-            .pull-right
-              = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
+            %li
+              %div
+                = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
 
 
       %h1.title= title
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index afd4f996b62c22bffa009aab6b8a42588147d48b..44d758dceb3190e5382b19383abd3d6a97e82eb5 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -24,12 +24,13 @@
           = f.password_field :current_password, required: true, class: 'form-control'
           %p.help-block
             You must provide your current password in order to change it.
-        .form-group
-          = f.label :password, 'New password', class: 'label-light'
-          = f.password_field :password, required: true, class: 'form-control'
-        .form-group
-          = f.label :password_confirmation, class: 'label-light'
-          = f.password_field :password_confirmation, required: true, class: 'form-control'
-        .prepend-top-default.append-bottom-default
-          = f.submit 'Save password', class: "btn btn-create append-right-10"
+      .form-group
+        = f.label :password, 'New password', class: 'label-light'
+        = f.password_field :password, required: true, class: 'form-control'
+      .form-group
+        = f.label :password_confirmation, class: 'label-light'
+        = f.password_field :password_confirmation, required: true, class: 'form-control'
+      .prepend-top-default.append-bottom-default
+        = f.submit 'Save password', class: "btn btn-create append-right-10"
+        - unless @user.password_automatically_set?
           = link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link"
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index cd582ba7060b7eb25e4b4f773078995a5d961a8a..dcb3be9585db98cc3cc4d2e055636140843aa6a8 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -26,7 +26,7 @@
         %a.btn.js-choose-user-avatar-button
           Browse file...
         %span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
-        = f.file_field :avatar, class: "js-user-avatar-input hidden"
+        = f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*"
       .help-block
         The maximum file size allowed is 200KB.
       - if @user.avatar?
@@ -94,3 +94,25 @@
       .prepend-top-default.append-bottom-default
         = f.submit 'Update profile settings', class: "btn btn-success"
         = link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
+
+.modal.modal-profile-crop
+  .modal-dialog
+    .modal-content
+      .modal-header
+        %button.close{:type => "button", :'data-dismiss' => "modal"}
+          %span
+            &times;
+        %h4.modal-title
+          Position and size your new avatar
+      .modal-body
+        .profile-crop-image-container
+          %img.modal-profile-crop-image
+        .crop-controls
+          .btn-group
+            %button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } }
+              %span.fa.fa-search-plus
+            %button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } }
+              %span.fa.fa-search-minus
+      .modal-footer
+        %button.btn.btn-primary.js-upload-user-avatar{:type => "button"}
+          Set new profile picture
diff --git a/app/views/projects/_builds_settings.html.haml b/app/views/projects/_builds_settings.html.haml
index 95ab9ecf3e842a3cb9c418e57300eaaa083ab8cf..9ae6964aaac044ed1e63dc8955d593d3c24016f4 100644
--- a/app/views/projects/_builds_settings.html.haml
+++ b/app/views/projects/_builds_settings.html.haml
@@ -1,6 +1,14 @@
 %fieldset.builds-feature
   %legend
     Builds:
+
+  - unless @repository.gitlab_ci_yml
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        %p Builds need to be configured before you can begin using Continuous Integration.
+        = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+        %hr
+
   .form-group
     .col-sm-offset-2.col-sm-10
       %p Get recent application code using the following command:
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 514cbfa339dccb16626eaa50ec71efcce02a669d..9b5de17dd3bb8867be7fe8a972575fc28745fbbb 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -5,7 +5,7 @@
   .cover-title.project-home-desc
     %h1
       = @project.name
-      %span.visibility-icon.has_tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
+      %span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
         = visibility_level_icon(@project.visibility_level, fw: false)
 
   - if @project.description.present?
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 7afea5a5049cd2ac7f5b5a18547ea1fb42aef956..88266e212301dbd19ff24d5bdeb12d9fc16b4618 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -16,7 +16,7 @@
         - else
           Name
         %b.caret
-      %ul.dropdown-menu
+      %ul.dropdown-menu.dropdown-menu-align-right
         %li
           = link_to namespace_project_branches_path(sort: nil) do
             Name
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 811d304ea7536957c91bb7c7dcde19b0581604bf..aa85f495e396ec3a998a8a94fbeeb85b988a9f8f 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -27,6 +27,9 @@
         = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project),
           data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
 
+      - unless @repository.gitlab_ci_yml
+        = link_to 'Get started with Builds', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+
       = link_to ci_lint_path, class: 'btn btn-default' do
         = icon('wrench')
         %span CI Lint
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index bac9e244d363b819b3318383c7cbb60c469b08f7..46e4de40042ff411bb3051ddd692f5b06d6509a4 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -5,10 +5,10 @@
   .panel-heading
     Commits (#{@commits.count})
   - if hidden > 0
-    %ul.well-list
+    %ul.content-list
       - commits.each do |commit|
         = render "projects/commits/inline_commit", commit: commit, project: @project
       %li.warning-row.unstyled
         #{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
   - else
-    %ul.well-list= render commits, project: @project
+    %ul.content-list= render commits, project: @project
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index a7e3c2478c24a0b6653811f7200b933c2212803b..64e8da9201d195e565bc92fe390a2a80913c49bc 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -12,7 +12,7 @@
       .light
         = pluralize(commits.count, 'commit')
     .col-md-10.col-sm-12
-      %ul.bordered-list
+      %ul.content-list
         = render commits, project: project
   %hr.lists-separator
 
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index e66e4669d487648495af378459f97c1c37e5615e..6da8e4f33a9ce9c77fd912a5060c9ad3c6f79b90 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,5 +1,5 @@
 - if current_user && can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
   .pull-right
-    = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn', title: @issue.to_branch_name do
+    = link_to namespace_project_branches_path(@project.namespace, @project, branch_name: @issue.to_branch_name, issue_iid: @issue.iid), method: :post, class: 'btn has-tooltip', title: @issue.to_branch_name do
       = icon('code-fork')
       New Branch
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index fde9304c0f89bef1d97c72000ef557b46312ae1a..efa7642b2dc5b9056db5680b8ac347535e262fce 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -11,6 +11,8 @@
     - if current_user
       = link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
         = icon('rss')
+        %span.icon-label
+          Subscribe
       = render 'shared/issuable/search_form', path: namespace_project_issues_path(@project.namespace, @project)
     - if can? current_user, :create_issue, @project
       = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: @issuable_finder.assignee.try(:id), milestone_id: @issuable_finder.milestones.try(:first).try(:id) }), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index 4927d239c1e1d3ae5f585b251a43bccdff7e83c9..0612863296ae3778a825f69993dcfb1ab313506a 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -8,7 +8,7 @@
 
     %strong.append-right-20
       = link_to_label(label) do
-        = pluralize label.open_issues_count, 'open issue'
+        = pluralize label.open_issues_count(current_user), 'open issue'
 
     - if current_user
       .label-subscription{data: {url: toggle_subscription_namespace_project_label_path(@project.namespace, @project, label)}}
diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml
index 667057ef2d8bfa867b12cbb1835393158186ef7d..093d1d1bb0f32ac7d58bf77b85b77bc80cc18ca4 100644
--- a/app/views/projects/tags/_download.html.haml
+++ b/app/views/projects/tags/_download.html.haml
@@ -6,7 +6,7 @@
     %span.caret
     %span.sr-only
       Select Archive Format
-  %ul.col-xs-10.dropdown-menu{ role: 'menu' }
+  %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
     %li
       = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
         %i.fa.fa-download
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 66b7ef996509a6ae4f570c8e67acf5500a811e9a..40c6eb9be45931113bd94a0975dcd3afbfca877b 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -21,7 +21,7 @@
       = icon('users')
       = number_with_delimiter(group.users.count)
 
-    %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
+    %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
       = visibility_level_icon(group.visibility_level, fw: false)
 
   = image_tag group_icon(group), class: "avatar s40 hidden-xs"
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index ac20f7d1f7e39d3d26a60cb830e2f7e8215da9fd..f91ff0e3694085894a27a7ad6e0fd4f130b548c2 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -31,18 +31,18 @@
       .issues_bulk_update.hide
         = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post  do
           .filter-item.inline
-            = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
+            = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
               %ul
                 %li
                   %a{href: "#", data: {id: "reopen"}} Open
                 %li
                   %a{href: "#", data: {id: "close"}} Closed
           .filter-item.inline
-            = dropdown_tag("Assignee", options: { toggle_class: "js-user-search", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
+            = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
               placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } })
           .filter-item.inline
-            = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable",
-              placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :js), use_id: true } })
+            = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
+              placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
           = hidden_field_tag 'update[issues_ids]', []
           = hidden_field_tag :state_event, params[:state_event]
           .filter-item.inline
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 551f0cc0b5121d5e6881e90293d8309827666ed5..178223fb4637f181a56ce9c69ac17fc9d694ae3f 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -127,11 +127,12 @@
       for this project.
 
   - if issuable.new_record?
-    = link_to 'Cancel', namespace_project_issues_path(@project.namespace, @project), class: 'btn btn-cancel'
+    = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
   - else
     .pull-right
       - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
-        = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), method: :delete, class: 'btn btn-grouped' do
+        = link_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
+                                                                                                  method: :delete, class: 'btn btn-grouped' do
           = icon('trash-o')
           Delete
-      = link_to 'Cancel', namespace_project_issue_path(@project.namespace, @project, issuable), class: 'btn btn-grouped btn-cancel'
+      = link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 876173151817caabf4ed92388f1a671d3f528856..006a34a11e3ea387f3e387ff0ef27a46c4112552 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -24,16 +24,21 @@
                 - else
                   View labels
     - if can? current_user, :admin_label, @project and @project
-      .dropdown-page-two
+      .dropdown-page-two.dropdown-new-label
         = dropdown_title("Create new label", back: true)
         = dropdown_content do
-          %input#new_label_color{type: "hidden"}
+          .dropdown-labels-error.js-label-error
           %input#new_label_name.dropdown-input-field{type: "text", placeholder: "Name new label"}
-          .dropdown-label-color-preview.js-dropdown-label-color-preview
           .suggest-colors.suggest-colors-dropdown
             - suggested_colors.each do |color|
               = link_to '#', style: "background-color: #{color}", data: { color: color } do
                 &nbsp
-          %button.btn.btn-primary.js-new-label-btn{type: "button"}
-            Create
+          .dropdown-label-color-input
+            .dropdown-label-color-preview.js-dropdown-label-color-preview
+            %input#new_label_color.dropdown-input-field{ type: "text" }
+          .clearfix
+            %button.btn.btn-primary.pull-left.js-new-label-btn{type: "button"}
+              Create
+            %button.btn.btn-default.pull-right.js-cancel-label-btn{type: "button"}
+              Cancel
     = dropdown_loading
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
index 3fb409ff7279c843b4c6c00110542b3361144b03..33a9a49485797daa07c483bb2173bbea72fc53cb 100644
--- a/app/views/shared/issuable/_participants.html.haml
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -17,4 +17,4 @@
         %a.js-participants-more{href: "#", data: {original_text: "+ #{participants_size - 7} more", less_text: "- show less"}}
           + #{participants_extra} more
 :javascript
-  Issue.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
+  IssuableContext.prototype.PARTICIPANTS_ROW_COUNT = #{participants_row};
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 2b95b19facc640f39a520e9009583ca82ae23185..0e20e86356da2fcd388ac283d18ccea6e4e52428 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -33,11 +33,11 @@
         .value.bold.hide-collapsed
           - if issuable.assignee
             = link_to_member(@project, issuable.assignee, size: 32) do
+              - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
+                %span.pull-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
+                  = icon('exclamation-triangle')
               %span.username
                 = issuable.assignee.to_reference
-            - if issuable.instance_of?(MergeRequest) && !issuable.can_be_merged_by?(issuable.assignee)
-              %a.pull-right.cannot-be-merged{href: '#', data: {toggle: 'tooltip'}, title: 'Not allowed to merge'}
-                = icon('exclamation-triangle')
           - else
             .light None
 
@@ -77,7 +77,7 @@
             Labels
             - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
               = link_to 'Edit', '#', class: 'edit-link pull-right'
-          .value.issuable-show-labels.hide-collapsed{class: ("has-labels" if issuable.labels.any?)}
+          .value.bold.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels.any?) }
             - if issuable.labels.any?
               - issuable.labels.each do |label|
                 = link_to_label(label, type: issuable.to_ability_name)
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 803dd95bc65ff8ae70a5bf3c0d07aca85809f472..53ff8959bc8c3ec2213148dc52e1850f92ed0f60 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -27,7 +27,7 @@
         %span
           = icon('star')
           = project.star_count
-      %span.visibility-icon.has_tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
+      %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)}
         = visibility_level_icon(project.visibility_level, fw: false)
 
     .title
diff --git a/config/routes.rb b/config/routes.rb
index 90d858d7fc101a671b6b5605905297a5d16c62af..6bf22fb4456775c4b3382c701f7202f9be87025b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -16,6 +16,18 @@ Rails.application.routes.draw do
     end
   end
 
+  # Make the built-in Rails routes available in development, otherwise they'd
+  # get swallowed by the `namespace/project` route matcher below.
+  #
+  # See https://git.io/va79N
+  if Rails.env.development?
+    get '/rails/mailers'         => 'rails/mailers#index'
+    get '/rails/mailers/:path'   => 'rails/mailers#preview'
+    get '/rails/info/properties' => 'rails/info#properties'
+    get '/rails/info/routes'     => 'rails/info#routes'
+    get '/rails/info'            => 'rails/info#index'
+  end
+
   namespace :ci do
     # CI API
     Ci::API::API.logger Rails.logger
@@ -351,11 +363,10 @@ Rails.application.routes.draw do
     get :issues
     get :merge_requests
     get :activity
-    get :labels
-    get :milestones
 
     scope module: :dashboard do
       resources :milestones, only: [:index, :show]
+      resources :labels, only: [:index]
 
       resources :groups, only: [:index]
       resources :snippets, only: [:index]
diff --git a/doc/README.md b/doc/README.md
index 08d0a6a5bfbd958d8e477442298360e93c9edca3..e6fa4fc049b2a4ffdfeb91019e7f965abbc406cc 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,7 +3,7 @@
 ## User documentation
 
 - [API](api/README.md) Automate GitLab via a simple and powerful API.
-- [CI](ci/README.md)
+- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, .gitlab-ci.yml options, and examples.
 - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
 - [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
 - [Importing to GitLab](workflow/importing/README.md).
@@ -45,4 +45,3 @@
   contributing to documentation.
 - [Development](development/README.md) Explains the architecture and the guidelines for shell commands.
 - [Legal](legal/README.md) Contributor license agreements.
-- [Release](release/README.md) How to make the monthly and security releases.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 762b35859b9cb6a69427e64e98058c8c6c7ac3fb..4316f3c1f6449f0e4bc33109777cfd0fb676210e 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -331,7 +331,7 @@ There are a few rules that apply to the usage of refs policy:
 * `only` and `except` are inclusive. If both `only` and `except` are defined
    in a job specification, the ref is filtered by `only` and `except`.
 * `only` and `except` allow the use of regular expressions.
-* `only` and `except` allow the use of special keywords: `branches` and `tags`.
+* `only` and `except` allow the use of special keywords: `branches`, `tags`, and `triggers`.
 * `only` and `except` allow to specify a repository path to filter jobs for
    forks.
 
@@ -348,6 +348,17 @@ job:
     - branches
 ```
 
+In this example, `job` will run only for refs that are tagged, or if a build is explicitly requested
+via an API trigger.
+
+```yaml
+job:
+  # use special keywords
+  only:
+    - tags
+    - triggers
+```
+
 The repository path can be used to have jobs executed only for the parent
 repository and not forks:
 
diff --git a/doc/development/scss_styleguide.md b/doc/development/scss_styleguide.md
index 6c48c25448bdc2fda7b71b27d89211a1c4b0830c..a79f4073cdec0d86fa1501e1e4be7e615f8a1164 100644
--- a/doc/development/scss_styleguide.md
+++ b/doc/development/scss_styleguide.md
@@ -72,9 +72,9 @@ p { margin: 0; padding: 0; }
 
 ### Colors
 
-HEX (hexadecimal) colors short-form should use shortform where possible, and 
-should use lower case letters to differenciate between letters and numbers, e.
-g. `#E3E3E3` vs. `#e3e3e3`.
+HEX (hexadecimal) colors should use shorthand where possible, and should use 
+lower case letters to differentiate between letters and numbers, e.g. `#E3E3E3`
+vs. `#e3e3e3`.
 
 ```scss
 // Bad
@@ -160,6 +160,7 @@ is slightly more performant.
 ```
 
 ### Selectors with a `js-` Prefix
+
 Do not use any selector prefixed with `js-` for styling purposes. These 
 selectors are intended for use only with JavaScript to allow for removal or 
 renaming without breaking styling.
@@ -187,8 +188,28 @@ CSSComb globally (system-wide). Run it in the GitLab directory with
 
 Note that this won't fix every problem, but it should fix a majority.
 
+### Ignoring issues
+
+If you want a line or set of lines to be ignored by the linter, you can use 
+`// scss-lint:disable RuleName` ([more info][disabling-linters]):
+
+```scss
+// This lint rule is disabled because the class name comes from a gem.
+// scss-lint:disable SelectorFormat
+.ui_charcoal {
+  background-color: #333;
+}
+// scss-lint:enable SelectorFormat
+```
+
+Make sure a comment is added on the line above the `disable` rule, otherwise the
+linter will throw a warning. `DisableLinterReason` is enabled to make sure the 
+style guide isn't being ignored, and to communicate to others why the style 
+guide is ignored in this instance.
+
 [csscomb]: https://github.com/csscomb/csscomb.js
 [node]: https://github.com/nodejs/node
 [npm]: https://www.npmjs.com/
 [scss-lint]: https://github.com/brigade/scss-lint
 [scss-lint-documentation]: https://github.com/brigade/scss-lint/blob/master/lib/scss_lint/linter/README.md
+[disabling-linters]: https://github.com/brigade/scss-lint#disabling-linters-via-source
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 493e1d1b09c1b5a1fe0e188343630b4d705f028a..3aa83975ace88d00953bc3445ea2c0257a8ea0e6 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -2,26 +2,14 @@
 
 Step-by-step guides on the basics of working with Git and GitLab.
 
-* [Start using Git on the command line](start-using-git.md)
-
-* [Create and add your SSH Keys](create-your-ssh-keys.md)
-
-* [Command Line basic commands](command-line-commands.md)
-
-* [Basic Git commands](basic-git-commands.md)
-
-* [Create a project](create-project.md)
-
-* [Create a group](create-group.md)
-
-* [Create a branch](create-branch.md)
-
-* [Fork a project](fork-project.md)
-
-* [Add a file](add-file.md)
-
-* [Add an image](add-image.md)
-
-* [Create a Merge Request](add-merge-request.md)
-
-* [Create an Issue](create-issue.md)
+- [Start using Git on the command line](start-using-git.md)
+- [Create and add your SSH Keys](create-your-ssh-keys.md)
+- [Command Line basics](command-line-commands.md)
+- [Create a project](create-project.md)
+- [Create a group](create-group.md)
+- [Create a branch](create-branch.md)
+- [Fork a project](fork-project.md)
+- [Add a file](add-file.md)
+- [Add an image](add-image.md)
+- [Create a Merge Request](add-merge-request.md)
+- [Create an Issue](create-issue.md)
diff --git a/doc/gitlab-basics/basic-git-commands.md b/doc/gitlab-basics/basic-git-commands.md
index 2b5767dd2d360dc1230779834c203d6aa2952432..c2a3415cbc4ef709256ed634ec16b027ad076833 100644
--- a/doc/gitlab-basics/basic-git-commands.md
+++ b/doc/gitlab-basics/basic-git-commands.md
@@ -1,59 +1,3 @@
 # Basic Git commands
 
-### Go to the master branch to pull the latest changes from there
-```
-git checkout master
-```
-
-### Download the latest changes in the project
-This is for you to work on an up-to-date copy (it is important to do every time you work on a project), while you setup tracking branches.
-```
-git pull REMOTE NAME-OF-BRANCH -u
-```
-(REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch)
-
-### Create a branch
-Spaces won't be recognized, so you need to use a hyphen or underscore.
-```
-git checkout -b NAME-OF-BRANCH
-```
-
-### Work on a branch that has already been created
-```
-git checkout NAME-OF-BRANCH
-```
-
-### View the changes you've made
-It's important to be aware of what's happening and what's the status of your changes.
-```
-git status
-```
-
-### Add changes to commit
-You'll see your changes in red when you type "git status".
-```
-git add CHANGES IN RED
-git commit -m "DESCRIBE THE INTENTION OF THE COMMIT"
-```
-
-### Send changes to gitlab.com
-```
-git push REMOTE NAME-OF-BRANCH
-```
-
-### Delete all changes in the Git repository, but leave unstaged things
-```
-git checkout .
-```
-
-### Delete all changes in the Git repository, including untracked files
-```
-git clean -f
-```
-
-### Merge created branch with master branch
-You need to be in the created branch.
-```
-git checkout NAME-OF-BRANCH
-git merge master
-```
+This section is now merged into [Start using Git](start-using-git.md).
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index b2ceda025c0887481b9f97d46c786d75f6074b2f..89ce8bcc3e88b217cb18362942f036da66fa9bf2 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -1,6 +1,7 @@
 # Start using Git on the command line
 
-If you want to start using a Git and GitLab, make sure that you have created an account on GitLab.
+If you want to start using a Git and GitLab, make sure that you have created an
+account on GitLab.
 
 ## Open a shell
 
@@ -59,3 +60,63 @@ To view the information that you entered, type:
 ```
 git config --global --list
 ```
+## Basic Git commands
+
+### Go to the master branch to pull the latest changes from there
+
+```
+git checkout master
+```
+
+### Download the latest changes in the project
+This is for you to work on an up-to-date copy (it is important to do every time you work on a project), while you setup tracking branches.
+```
+git pull REMOTE NAME-OF-BRANCH -u
+```
+(REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch)
+
+### Create a branch
+Spaces won't be recognized, so you need to use a hyphen or underscore.
+```
+git checkout -b NAME-OF-BRANCH
+```
+
+### Work on a branch that has already been created
+```
+git checkout NAME-OF-BRANCH
+```
+
+### View the changes you've made
+It's important to be aware of what's happening and what's the status of your changes.
+```
+git status
+```
+
+### Add changes to commit
+You'll see your changes in red when you type "git status".
+```
+git add CHANGES IN RED
+git commit -m "DESCRIBE THE INTENTION OF THE COMMIT"
+```
+
+### Send changes to gitlab.com
+```
+git push REMOTE NAME-OF-BRANCH
+```
+
+### Delete all changes in the Git repository, but leave unstaged things
+```
+git checkout .
+```
+
+### Delete all changes in the Git repository, including untracked files
+```
+git clean -f
+```
+
+### Merge created branch with master branch
+You need to be in the created branch.
+```
+git checkout NAME-OF-BRANCH
+git merge master
+```
diff --git a/doc/intro/README.md b/doc/intro/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..fecbbe6317b0a63c4585da46d253db3f67845c1a
--- /dev/null
+++ b/doc/intro/README.md
@@ -0,0 +1,41 @@
+# Get started with GitLab
+
+## Organize
+
+Create projects and groups.
+
+- [Create a new project](../gitlab-basics/create-project.md)
+- [Create a new group](../gitlab-basics/create-group.md)
+
+## Prioritize
+
+Create issues, labels, milestones, cast your vote, and review issues.
+
+- [Create a new issue](../gitlab-basics/create-issue.md)
+- [Assign labels to issues](../workflow/labels.md)
+- [Use milestones as an overview of your project's tracker](../workflow/milestones.md)
+- [Use voting to express your like/dislike to issues and merge requests](../workflow/award_emoji.md)
+
+## Collaborate
+
+Create merge requests and review code.
+
+- [Fork a project and contribute to it](../workflow/forking_workflow.md)
+- [Create a new merge request](../gitlab-basics/add-merge-request.md)
+- [Automatically close issues from merge requests](../customization/issue_closing.md)
+- [Automatically merge when your builds succeed](../workflow/merge_when_build_succeeds.md)
+- [Revert any commit](../workflow/revert_changes.md)
+
+## Test and Deploy
+
+Use the built-in continuous integration in GitLab.
+
+- [Get started with GitLab CI](../ci/quick_start/README.md)
+
+## Install and Update
+
+Install and update your GitLab installation.
+
+- [Install GitLab](https://about.gitlab.com/installation/)
+- [Update GitLab](https://about.gitlab.com/update/)
+- [Explore Omnibus GitLab configuration options](http://doc.gitlab.com/omnibus/settings/configuration.html)
diff --git a/doc/monitoring/performance/grafana_configuration.md b/doc/monitoring/performance/grafana_configuration.md
new file mode 100644
index 0000000000000000000000000000000000000000..416c9870aa081251adaba4893315e8781744fa33
--- /dev/null
+++ b/doc/monitoring/performance/grafana_configuration.md
@@ -0,0 +1,118 @@
+# Grafana Configuration
+
+[Grafana](http://grafana.org/) is a tool that allows you to visualize time
+series metrics through graphs and dashboards. It supports several backend
+data stores, including InfluxDB. GitLab writes performance data to InfluxDB
+and Grafana will allow you to query InfluxDB to display useful graphs.
+
+For the easiest installation and configuration, install Grafana on the same
+server as InfluxDB. For larger installations, you may want to split out these
+services.
+
+## Installation
+
+Grafana supplies package repositories (Yum/Apt) for easy installation.
+See [Grafana installation documentation](http://docs.grafana.org/installation/)
+for detailed steps.
+
+> **Note**: Before starting Grafana for the first time, set the admin user
+and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
+will be `admin`.
+
+## Configuration
+
+Login as the admin user. Expand the menu by clicking the Grafana logo in the
+top left corner. Choose 'Data Sources' from the menu. Then, click 'Add new'
+in the top bar.
+
+![Grafana empty data source page](img/grafana_data_source_empty.png)
+
+Fill in the configuration details for the InfluxDB data source. Save and
+Test Connection to ensure the configuration is correct.
+
+- **Name**: InfluxDB
+- **Default**: Checked
+- **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x)
+- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB
+on a separate server)
+- **Access**: proxy
+- **Database**: gitlab
+- **User**: admin (Or the username configured when setting up InfluxDB)
+- **Password**: The password configured when you set up InfluxDB
+
+![Grafana data source configurations](img/grafana_data_source_configuration.png)
+
+## Apply retention policies and create continuous queries
+
+If you intend to import the GitLab provided Grafana dashboards, you will need
+to copy and run a set of queries against InfluxDB to create the needed data
+sets.
+
+On the InfluxDB server, run the following command, substituting your InfluxDB
+user and password:
+
+```bash
+influxdb --username admin -password super_secret
+```
+
+This will drop you in to an InfluxDB interactive session. Copy the entire
+contents below and paste it in to the interactive session:
+
+```
+CREATE RETENTION POLICY gitlab_30d ON gitlab DURATION 30d REPLICATION 1 DEFAULT
+CREATE RETENTION POLICY seven_days ON gitlab DURATION 7d REPLICATION 1
+CREATE CONTINUOUS QUERY rails_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.rails_transaction_timings FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_transaction_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean", percentile(sql_duration, 95.000) AS "sql_duration_95th", percentile(sql_duration, 99.000) AS "sql_duration_99th", mean(sql_duration) AS "sql_duration_mean", percentile(view_duration, 95.000) AS "view_duration_95th", percentile(view_duration, 99.000) AS "view_duration_99th", mean(view_duration) AS "view_duration_mean" INTO gitlab.seven_days.sidekiq_transaction_timings FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.rails_transaction_counts FROM gitlab.gitlab_30d.rails_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_transaction_counts_seven_days ON gitlab BEGIN SELECT count("duration") AS "count" INTO gitlab.seven_days.sidekiq_transaction_counts FROM gitlab.gitlab_30d.sidekiq_transactions GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.rails_method_call_timings FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_method_call_timings_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS "duration_95th", percentile("duration", 99.000) AS "duration_99th", mean("duration") AS "duration_mean" INTO gitlab.seven_days.sidekiq_method_call_timings FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.rails_method_call_timings_per_method FROM gitlab.gitlab_30d.rails_method_calls GROUP BY time(1m), method END
+CREATE CONTINUOUS QUERY sidekiq_method_call_timings_per_method_seven_days ON gitlab BEGIN SELECT percentile("duration", 95.000) AS duration_95th, percentile("duration", 99.000) AS duration_99th, mean("duration") AS duration_mean INTO gitlab.seven_days.sidekiq_method_call_timings_per_method FROM gitlab.gitlab_30d.sidekiq_method_calls GROUP BY time(1m), method END
+CREATE CONTINUOUS QUERY rails_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.rails_memory_usage_per_minute FROM gitlab.gitlab_30d.rails_memory_usage GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_memory_usage_per_minute ON gitlab BEGIN SELECT percentile(value, 95.000) AS memory_95th, percentile(value, 99.000) AS memory_99th, mean(value) AS memory_mean INTO gitlab.seven_days.sidekiq_memory_usage_per_minute FROM gitlab.gitlab_30d.sidekiq_memory_usage GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.sidekiq_file_descriptors_per_minute FROM gitlab.gitlab_30d.sidekiq_file_descriptors GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_file_descriptors_per_minute ON gitlab BEGIN SELECT sum(value) AS value INTO gitlab.seven_days.rails_file_descriptors_per_minute FROM gitlab.gitlab_30d.rails_file_descriptors GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.rails_gc_counts_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_gc_counts_per_minute ON gitlab BEGIN SELECT sum(count) AS count INTO gitlab.seven_days.sidekiq_gc_counts_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.rails_gc_timings_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_gc_timings_per_minute ON gitlab BEGIN SELECT percentile(total_time, 95.000) AS duration_95th, percentile(total_time, 99.000) AS duration_99th, mean(total_time) AS duration_mean INTO gitlab.seven_days.sidekiq_gc_timings_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY rails_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.rails_gc_major_minor_per_minute FROM gitlab.gitlab_30d.rails_gc_statistics GROUP BY time(1m) END
+CREATE CONTINUOUS QUERY sidekiq_gc_major_minor_per_minute ON gitlab BEGIN SELECT sum(major_gc_count) AS major, sum(minor_gc_count) AS minor INTO gitlab.seven_days.sidekiq_gc_major_minor_per_minute FROM gitlab.gitlab_30d.sidekiq_gc_statistics GROUP BY time(1m) END
+```
+
+## Import Dashboards
+
+You can now import a set of default dashboards that will give you a good
+start on displaying useful information. GitLab has published a set of default
+[Grafana dashboards][grafana-dashboards] to get you started. Clone the
+repository or download a zip/tarball, then follow these steps to import each
+JSON file.
+
+Open the dashboard dropdown menu and click 'Import'
+
+![Grafana dashboard dropdown](/img/grafana_dashboard_dropdown.png)
+
+Click 'Choose file' and browse to the location where you downloaded or cloned
+the dashboard repository. Pick one of the JSON files to import.
+
+![Grafana dashboard import](/img/grafana_dashboard_import.png)
+
+Once the dashboard is imported, be sure to click save icon in the top bar. If
+you do not save the dashboard after importing it will be removed when you
+navigate away.
+
+![Grafana save icon](/img/grafana_save_icon.png)
+
+Repeat this process for each dashboard you wish to import.
+
+[grafana-dashboards]: https://gitlab.com/gitlab-org/grafana-dashboards
+
+---
+
+Read more on:
+
+- [Introduction to GitLab Performance Monitoring](introduction.md)
+- [GitLab Configuration](gitlab_configuration.md)
+- [InfluxDB Installation/Configuration](influxdb_configuration.md)
+- [InfluxDB Schema](influxdb_schema.md)
diff --git a/doc/monitoring/performance/img/grafana_dashboard_dropdown.png b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4448c7a09f42f3480f8bfbd513cfb8bd7dbfe3c
Binary files /dev/null and b/doc/monitoring/performance/img/grafana_dashboard_dropdown.png differ
diff --git a/doc/monitoring/performance/img/grafana_dashboard_import.png b/doc/monitoring/performance/img/grafana_dashboard_import.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a2d3c0937a8d0fda014c65e079ccd987800982a
Binary files /dev/null and b/doc/monitoring/performance/img/grafana_dashboard_import.png differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_configuration.png b/doc/monitoring/performance/img/grafana_data_source_configuration.png
new file mode 100644
index 0000000000000000000000000000000000000000..7e2e111f570468652fdbf3f96be0b0387de6fb28
Binary files /dev/null and b/doc/monitoring/performance/img/grafana_data_source_configuration.png differ
diff --git a/doc/monitoring/performance/img/grafana_data_source_empty.png b/doc/monitoring/performance/img/grafana_data_source_empty.png
new file mode 100644
index 0000000000000000000000000000000000000000..11e27571e64a4cff578c5ad7311f95d7ae10d54c
Binary files /dev/null and b/doc/monitoring/performance/img/grafana_data_source_empty.png differ
diff --git a/doc/monitoring/performance/img/grafana_save_icon.png b/doc/monitoring/performance/img/grafana_save_icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d4265bee8e08e369f0ac0de9221835a214553a3
Binary files /dev/null and b/doc/monitoring/performance/img/grafana_save_icon.png differ
diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md
index f2460d3130203f33379701a01a408f54a246ab44..79904916b7e2bdb08719bf1d3186e0cc337f6e6f 100644
--- a/doc/monitoring/performance/introduction.md
+++ b/doc/monitoring/performance/introduction.md
@@ -8,8 +8,9 @@ Apart from this introduction, you are advised to read through the following
 documents in order to understand and properly configure GitLab Performance Monitoring:
 
 - [GitLab Configuration](gitlab_configuration.md)
-- [InfluxDB Configuration](influxdb_configuration.md)
+- [InfluxDB Install/Configuration](influxdb_configuration.md)
 - [InfluxDB Schema](influxdb_schema.md)
+- [Grafana Install/Configuration](grafana_configuration.md)
 
 ## Introduction to GitLab Performance Monitoring
 
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index 6e22ea7b72af8780ea6fba1fdb5a46213f8c584a..20aa90f0d697e148397fc26be3dfdeca99398249 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -35,6 +35,21 @@ the repository.
 1. Go to your project's **Settings**
 1. Change "Visibility Level" to either Public, Internal or Private
 
+## Visibility of groups
+
+>**Note:**
+[Starting with][3323] GitLab 8.6, the group visibility has changed and can be
+configured the same way as projects. In previous versions, a group's page was
+always visible to all users.
+
+Like with projects, the visibility of a group can be set to dictate whether
+anonymous users, all signed in users, or only explicit group members can view
+it. The restriction for visibility levels on the application setting level also
+applies to groups, so if that's set to internal, the explore page will be empty
+for anonymous users. The group page now has a visibility level icon.
+
+[3323]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3323
+
 ## Visibility of users
 
 The public page of a user, located at `/u/username`, is always visible whether
@@ -43,14 +58,6 @@ you are logged in or not.
 When visiting the public page of a user, you can only see the projects which
 you are privileged to.
 
-## Visibility of groups
-
-The public page of a group, located at `/groups/groupname`, is always visible
-to everyone.
-
-Logged out users will be able to see the description and the avatar of the
-group as well as all public projects belonging to that group.
-
 ## Restricting the use of public or internal projects
 
 In the Admin area under **Settings** (`/admin/application_settings`), you can
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index f6d1234ac4ad6545658f8dc0000f596721e6f2df..4329ac30a1c38269d223431af8e41df1180f9a38 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -249,6 +249,9 @@ reconfigure` after changing `gitlab-secrets.json`.
 ### Installation from source
 
 ```
+# Stop processes that are connected to the database
+sudo service gitlab stop
+
 bundle exec rake gitlab:backup:restore RAILS_ENV=production
 ```
 
diff --git a/doc/release/README.md b/doc/release/README.md
deleted file mode 100644
index 52eca7c02a61e7c2043893f4fc3115f65c52e85d..0000000000000000000000000000000000000000
--- a/doc/release/README.md
+++ /dev/null
@@ -1,10 +0,0 @@
-## Release cycle
-
-Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG).  Features that will likely be in the next releases can be found on the [direction page](https://about.gitlab.com/direction/).
-
-## Release process documentation
-
-- [Monthly release](monthly.md), every month on the 22nd.
-- [Patch release](patch.md), if there are serious regressions.
-- [Security](security.md), for security problems.
-- [Master](master.md), update process for the master branch.
diff --git a/doc/release/howto_rc1.md b/doc/release/howto_rc1.md
deleted file mode 100644
index 07c703142d4d3dea5b5103f9e246f09a9080bfa5..0000000000000000000000000000000000000000
--- a/doc/release/howto_rc1.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# How to create RC1
-
-The RC1 release comes with the task to update the installation and upgrade docs. Be mindful that there might already be merge requests for this on GitLab or GitHub.
-
-### 1. Update the installation guide
-
-1. Check if it references the correct branch `x-x-stable` (doesn't exist yet, but that is okay)
-1. Check the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782)
-1. Check the [Git version](/lib/tasks/gitlab/check.rake#L794)
-1. There might be other changes. Ask around.
-
-### 2. Create update guides
-
-[Follow this guide](howto_update_guides.md) to create update guides.
-
-### 3. Code quality indicators
-
-Make sure the code quality indicators are green / good.
-
-- [![Build status](http://ci.gitlab.org/projects/1/status.png?ref=master)](http://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch)
-
-- [![Build Status](https://semaphoreapp.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/243338/badge.png)](https://semaphoreapp.com/gitlabhq/gitlabhq) (master branch)
-
-- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.png)](https://codeclimate.com/github/gitlabhq/gitlabhq)
-
-- [![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.png)](https://gemnasium.com/gitlabhq/gitlabhq) this button can be yellow (small updates are available) but must not be red (a security fix or an important update is available)
-
-- [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq)
-
-### 4. Run release tool
-
-**Make sure EE `master` has latest changes from CE `master`**
-
-Get release tools
-
-```
-git clone git@dev.gitlab.org:gitlab/release-tools.git
-cd release-tools
-```
-
-Release candidate creates stable branch from master.
-So we need to sync master branch between all CE, EE and CI remotes.
-
-```
-bundle exec rake sync
-```
-
-Create release candidate and stable branch:
-
-```
-bundle exec rake release["x.x.0.rc1"]
-```
-
-Now developers can use master for merging new features.
-So you should use stable branch for future code changes related to release.
diff --git a/doc/release/howto_update_guides.md b/doc/release/howto_update_guides.md
deleted file mode 100644
index 23d0959c33d590a4a15fa6ee2130184f7f88d176..0000000000000000000000000000000000000000
--- a/doc/release/howto_update_guides.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# Create update guides
-
-1. Create: CE update guide from previous version. Like `7.3-to-7.4.md`
-1. Create: CE to EE update guide in EE repository for latest version.
-1. Update: `6.x-or-7.x-to-7.x.md` to latest version.
-1. Create: CI update guide from previous version
-
-It's best to copy paste the previous guide and make changes where necessary.
-The typical steps are listed below with any points you should specifically look at.
-
-#### 0. Any major changes?
-
-List any major changes here, so the user is aware of them before starting to upgrade. For instance:
-
-- Database updates
-- Web server changes
-- File structure changes
-
-#### 1. Stop server
-
-#### 2. Make backup
-
-#### 3. Do users need to update dependencies like `git`?
-
-- Check if the [GitLab Shell version](/lib/tasks/gitlab/check.rake#L782) changed since the last release.
-
-- Check if the [Git version](/lib/tasks/gitlab/check.rake#L794) changed since the last release.
-
-#### 4. Get latest code
-
-#### 5. Does GitLab shell need to be updated?
-
-#### 6. Install libs, migrations, etc.
-
-#### 7. Any config files updated since last release?
-
-Check if any of these changed since last release:
-
-- [lib/support/nginx/gitlab](/lib/support/nginx/gitlab)
-- [lib/support/nginx/gitlab-ssl](/lib/support/nginx/gitlab-ssl)
-- <https://gitlab.com/gitlab-org/gitlab-shell/commits/master/config.yml.example>
-- [config/gitlab.yml.example](/config/gitlab.yml.example)
-- [config/unicorn.rb.example](/config/unicorn.rb.example)
-- [config/database.yml.mysql](/config/database.yml.mysql)
-- [config/database.yml.postgresql](/config/database.yml.postgresql)
-- [config/initializers/rack_attack.rb.example](/config/initializers/rack_attack.rb.example)
-- [config/resque.yml.example](/config/resque.yml.example)
-
-#### 8. Need to update init script?
-
-Check if the `init.d/gitlab` script changed since last release: [lib/support/init.d/gitlab](/lib/support/init.d/gitlab)
-
-#### 9. Start application
-
-#### 10. Check application status
diff --git a/doc/release/master.md b/doc/release/master.md
deleted file mode 100644
index 9163e6520030796ab7545cd7c0f0f67b11df5371..0000000000000000000000000000000000000000
--- a/doc/release/master.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# How to push GitLab CE master branch to all remotes.
-
-The source code of GitLab is available on multiple servers (with GitLab.com as the canonical source).
-Synchronization between the repo's is done by the lead developer if there is no rush.
-This happens a few times per workday on average.
-If somebody else with access to all repo's wants to do it the instructions are below.
-This is just to distribute changes, not to make them.
-
-## Add this to `.bashrc` or [your dotfiles](https://github.com/dosire/dotfiles/commit/52803ce3ac60d57632164b7713ff0041e86fa26c)
-
-```bash
-gpa ()
-{
-  git push origin ${1:-master} && git push gh ${1:-master} && git push gl ${1:-master}
-}
-```
-
-## Then add remotes to your local repo
-
-```bash
-cd my-gitlab-ce-repo
-
-git remote add origin git@dev.gitlab.org:gitlab/gitlabhq.git
-git remote add gh git@github.com:gitlabhq/gitlabhq.git
-git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git
-```
-
-## Push to all remotes
-
-```bash
-gpa
-```
-
-# Yanking packages from packages.gitlab.com
-
-In case something went wrong with the release and there is a need to remove the packages you can yank the packages by following the
-procedure described in [package cloud documentation](https://packagecloud.io/docs#yank_pkg).
-
-You need to have:
-
-1. `package_cloud` gem installed (sudo gem install package_cloud)
-1. Email and password for packages.gitlab.com
-1. Make sure that you are supplying the url to packages.gitlab.com (default is packagecloud.io)
-
-Example of yanking a package:
-
-```bash
-package_cloud yank --url https://packages.gitlab.com gitlab/gitlab-ce/el/6 gitlab-ce-7.10.2~omnibus-1.x86_64.rpm
-```
-
-If you are attempting this for the first time the output will look something like:
-
-```bash
-Looking for repository at gitlab/gitlab-ce... No config file exists at /Users/marin/.packagecloud. Login to create one.
-Email:
-marin@gitlab.com
-Password:
-
-Got your token. Writing a config file to /Users/marin/.packagecloud... success!
-success!
-Attempting to yank package at gitlab/gitlab-ce/el/6/gitlab-ce-7.10.2~omnibus-1.x86_64.rpm...done!
-```
diff --git a/doc/release/monthly.md b/doc/release/monthly.md
deleted file mode 100644
index 907c19e65a0300d795367c9e8409564a5fdc4c45..0000000000000000000000000000000000000000
--- a/doc/release/monthly.md
+++ /dev/null
@@ -1,245 +0,0 @@
-# Monthly Release
-
-NOTE: This is a guide used by the GitLab the company to release GitLab.
-As an end user you do not need to use this guide.
-
-The process starts 7 working days before the release.
-The release manager doesn't have to perform all the work but must ensure someone is assigned.
-The current release manager must schedule the appointment of the next release manager.
-The new release manager should create overall issue to track the progress.
-The release manager should be the only person pushing/merging commits to the x-y-stable branches.
-
-## Release Manager
-
-A release manager is selected that coordinates all releases the coming month,
-including the patch releases for previous releases.
-The release manager has to make sure all the steps below are done and delegated where necessary.
-This person should also make sure this document is kept up to date and issues are created and updated.
-
-## Take vacations into account
-
-The time is measured in weekdays to compensate for weekends.
-Do everything on time to prevent problems due to rush jobs or too little testing time.
-Make sure that you take into account any vacations of maintainers.
-If the release is falling behind immediately warn the team.
-
-## Create an overall issue and follow it
-
-Create an issue in the GitLab CE project. Name it "Release x.x" and tag it with
-the `release` label for easier searching. Replace the dates with actual dates
-based on the number of workdays before the release. All steps from issue
-template are explained below:
-
-```
-### Xth: (7 working days before the 22nd)
-
-- [ ] Triage the [Omnibus milestone]
-
-### Xth: (6 working days before the 22nd)
-
-- [ ] Determine QA person and notify this person
-- [ ] Check the tasks in [how to rc1 guide](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/release/howto_rc1.md) and delegate tasks if necessary
-- [ ] Merge CE `master` into EE `master` via merge request (#LINK)
-- [ ] Create CE and EE RC1 versions (#LINK)
-- [ ] Build RC1 packages
-
-### Xth: (5 working days before the 22nd)
-
-- [ ] Do QA and fix anything coming out of it (#LINK)
-- [ ] Close the [Omnibus milestone]
-- [ ] Prepare the [blog post]
-
-### Xth: (4 working days before the 22nd)
-
-- [ ] Update GitLab.com with RC1
-- [ ] Create the regression issue in the CE issue tracker:
-
-    ```
-    This is a meta issue to index possible regressions in this monthly release
-    and any patch versions.
-
-    Please do not raise or discuss issues directly in this issue but link to
-    issues that might warrant a patch release. If there is a Merge Request
-    that fixes the issue, please link to that as well.
-
-    Please only post one regression issue and/or merge request per comment.
-    Comments will be updated by the release manager as they are addressed.
-    ```
-
-- [ ] Tweet about RC1 release:
-
-    ```
-    GitLab x.y.0.rc1 is available: https://packages.gitlab.com/gitlab/unstable
-    Use at your own risk. Please link regressions issues from
-    LINK_TO_REGRESSION_ISSUE
-    ```
-
-### Xth: (3 working days before the 22nd)
-
-- [ ] Merge `x-y-stable` into `x-y-stable-ee`
-- [ ] Check that everyone is mentioned on the [blog post] using `@all`
-
-### Xth: (2 working days before the 22nd)
-
-- [ ] Check that MVP is added to the [MVP page]
-
-### Xth: (1 working day before the 22nd)
-
-- [ ] Merge `x-y-stable` into `x-y-stable-ee`
-- [ ] Create CE and EE release candidates
-- [ ] Create Omnibus tags and build packages for the latest release candidates
-- [ ] Update GitLab.com with the latest RC
-
-### 22nd before 1200 CET:
-
-Release before 1200 CET / 2AM PST, to make sure the majority of our users
-get the new version on the 22nd and there is sufficient time in the European
-workday to quickly fix any issues.
-
-- [ ] Merge `x-y-stable` into `x-y-stable-ee`
-- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools)
-- [ ] Create the 'x.y.0' version on version.gitlab.com
-- [ ] Try to do before 1100 CET: Create and push Omnibus tags for x.y.0 (will auto-release the packages)
-- [ ] Try to do before 1200 CET: Publish the release [blog post]
-- [ ] Tweet about the release
-- [ ] Schedule a second Tweet of the release announcement with the same text at 1800 CET / 8AM PST
-
-[Omnibus milestone]: LINK_TO_OMNIBUS_MILESTONE
-[blog post]: LINK_TO_WIP_BLOG_POST
-[MVP page]: https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/source/mvp/index.html
-```
-
-- - -
-
-## Update changelog
-
-Any changes not yet added to the changelog are added by lead developer and in that merge request the complete team is
-asked if there is anything missing.
-
-There are three changelogs that need to be updated: CE, EE and CI.
-
-## Create RC1 (CE, EE, CI)
-
-[Follow this How-to guide](howto_rc1.md) to create RC1.
-
-## Prepare CHANGELOG for next release
-
-Once the stable branches have been created, update the CHANGELOG in `master` with the upcoming version, usually X.X.X.pre.
-
-On creating the stable branches, notify the core team and developers.
-
-## QA
-
-Create issue on dev.gitlab.org `gitlab` repository, named "GitLab X.X QA" in order to keep track of the progress.
-
-Use the omnibus packages created for RC1 of Enterprise Edition using [this guide](https://dev.gitlab.org/gitlab/gitlab-ee/blob/master/doc/release/manual_testing.md).
-
-**NOTE** Upgrader can only be tested when tags are pushed to all repositories. Do not forget to confirm it is working before releasing. Note that in the issue.
-
-#### Fix anything coming out of the QA
-
-Create an issue with description of a problem, if it is quick fix fix it yourself otherwise contact the team for advice.
-
-**NOTE** If there is a problem that cannot be fixed in a timely manner, reverting the feature is an option! If the feature is reverted,
-create an issue about it in order to discuss the next steps after the release.
-
-## Update GitLab.com with RC1
-
-Use the omnibus EE packages created for RC1.
-If there are big database migrations consider testing them with the production db on a VM.
-Try to deploy in the morning.
-It is important to do this as soon as possible, so we can catch any errors before we release the full version.
-
-## Create a regressions issue
-
-On [the GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues/) create an issue titled "GitLab X.X regressions" add the following text:
-
-This is a meta issue to discuss possible regressions in this monthly release and any patch versions.
-Please do not raise issues directly in this issue but link to issues that might warrant a patch release.
-The decision to create a patch release or not is with the release manager who is assigned to this issue.
-The release manager will comment here about the plans for patch releases.
-
-Assign the issue to the release manager and at mention all members of GitLab core team. If there are any known bugs in the release add them immediately.
-
-## Tweet about RC1
-
-Tweet about the RC release:
-
-> GitLab x.x.0.rc1 is out. This release candidate is only suitable for testing. Please link regressions issues from LINK_TO_REGRESSION_ISSUE
-
-## Prepare the blog post
-
-1. The blog post template for this release should already exist and might have comments that were added during the month.
-1. Fill out as much of the blog post template as you can.
-1. Make sure the blog post contains information about the GitLab CI release.
-1. Check the changelog of CE and EE for important changes.
-1. Also check the CI changelog
-1. Add a proposed tweet text to the blog post WIP MR description.
-1. Create a WIP MR for the blog post
-1. Make sure merge request title starts with `WIP` so it can not be accidentally merged until ready.
-1. Ask Dmitriy (or a team member with OS X) to add screenshots to the WIP MR.
-1. Decide with core team who will be the MVP user.
-1. Create WIP MR for adding MVP to MVP page on website
-1. Add a note if there are security fixes: This release fixes an important security issue and we advise everyone to upgrade as soon as possible.
-1. Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master)
-1. Assign to one reviewer who will fix spelling issues by editing the branch (either with a git client or by using the online editor)
-1. Comment to the reviewer: '@person Please mention the whole team as soon as you are done (3 workdays before release at the latest)'
-1. Create a new merge request with complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) for the next release using the branch name `release-x-x-x`.
-
-## Create CE, EE, CI stable versions
-
-Get release tools
-
-```
-git clone git@dev.gitlab.org:gitlab/release-tools.git
-cd release-tools
-```
-
-Bump version, create release tag and push to remotes:
-
-```
-bundle exec rake release["x.x.0"]
-```
-
-This will create correct version and tag and push to all CE, EE and CI remotes.
-
-Update [installation.md](/doc/install/installation.md) to the newest version in master.
-
-
-## Create Omnibus tags and build packages
-
-Follow the [release doc in the Omnibus repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md).
-This can happen before tagging because Omnibus uses tags in its own repo and SHA1's to refer to the GitLab codebase.
-
-## Update GitLab.com with the stable version
-
-- Deploy the package (should not need downtime because of the small difference with RC1)
-- Deploy the package for gitlab.com/ci
-
-## Release CE, EE and CI
-
-__1. Publish packages for new release__
-
-Update `downloads/index.html` and `downloads/archive/index.html` in `www-gitlab-com` repository.
-
-__2. Publish blog for new release__
-
-Doublecheck the everyone has been mentioned in the blog post.
-Merge the [blog merge request](#1-prepare-the-blog-post) in `www-gitlab-com` repository.
-
-__3. Tweet to blog__
-
-Send out a tweet to share the good news with the world.
-List the most important features and link to the blog post.
-
-Proposed tweet "Release of GitLab X.X & CI Y.Y! FEATURE, FEATURE and FEATURE &lt;link-to-blog-post&gt; #gitlab"
-
-Consider creating a post on Hacker News.
-
-## Release new AMIs
-
-[Follow this guide](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
-
-## Create a WIP blogpost for the next release
-
-Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md).
diff --git a/doc/release/patch.md b/doc/release/patch.md
deleted file mode 100644
index 1c921439156aa118ec7895cfa179ba99db104b8d..0000000000000000000000000000000000000000
--- a/doc/release/patch.md
+++ /dev/null
@@ -1,81 +0,0 @@
-# Things to do when doing a patch release
-
-NOTE: This is a guide for GitLab developers. If you are trying to install GitLab
-see the latest stable [installation guide](install/installation.md) and if you
-are trying to upgrade, see the [upgrade guides](update).
-
-## When to do a patch release
-
-Patch releases are done as-needed in order to fix regressions in the current
-major release that cannot or should not wait until the next major release.
-What's included and when to release is at the discretion of the release manager.
-
-## Release Procedure
-
-### Create a patch issue
-
-Create an issue in the GitLab CE project. Name it "Release x.y.z", tag it with
-the `release` label, and assign it to the milestone of the corresponding major
-release.
-
-Use the following template:
-
-```
-- Picked into respective `stable` branches:
-- [ ] Merge `x-y-stable` into `x-y-stable-ee`
-- [ ] release-tools: `x.y.z`
-- omnibus-gitlab
-  - [ ] `x.y.z+ee.0`
-  - [ ] `x.y.z+ce.0`
-- [ ] Deploy
-- [ ] Add patch notice to [x.y regressions]()
-- [ ] [Blog post]()
-- [ ] [Tweet]()
-- [ ] Add entry to version.gitlab.com
-```
-
-Update the issue with links to merge requests that need to be/have been picked
-into the `stable` branches.
-
-### Preparation
-
-1. Verify that the issue can be reproduced
-1. Note in the 'GitLab X.X regressions' that you will create a patch
-1. Fix the issue on a feature branch, do this on the private GitLab development server
-1. If it is a security issue, then assign it to the release manager and apply a 'security' label
-1. Consider creating and testing workarounds
-1. After the branch is merged into master, cherry pick the commit(s) into the current stable branch
-1. Make sure that the build has passed and all tests are passing
-1. In a separate commit in the master branch update the CHANGELOG
-1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X"
-1. Merge CE stable branch into EE stable branch
-
-### Bump version
-
-Get release tools
-
-```
-git clone git@dev.gitlab.org:gitlab/release-tools.git
-cd release-tools
-```
-
-Bump all versions in stable branch, even if the changes affect only EE, CE, or CI. Since all the versions are synced now,
-it doesn't make sense to say upgrade CE to 7.2, EE to 7.3 and CI to 7.1.
-
-Create release tag and push to remotes:
-
-```
-bundle exec rake release["x.x.x"]
-```
-
-## Release
-
-1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md)
-1. Apply the patch to GitLab.com and the private GitLab development server
-1. Apply the patch to ci.gitLab.com and the private GitLab CI development server
-1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md)
-1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post
-1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only)
-1. Create the 'x.y.0' version on version.gitlab.com
-1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
-1. Create a new patch release issue for the next potential release
diff --git a/doc/release/security.md b/doc/release/security.md
deleted file mode 100644
index 118c016ba4f2bc54a760383a718b549b8c720139..0000000000000000000000000000000000000000
--- a/doc/release/security.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Things to do when doing an out-of-bound security release
-
-NOTE: This is a guide for GitLab developers. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update).
-
-## When to do a security release
-
-Do a security release when there is a critical issue that needs to be addresses before the next monthly release. Otherwise include it in the monthly release and note there was a security fix in the release announcement.
-
-## Security vulnerability disclosure
-
-Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
-
-## Release Procedure
-
-1. Verify that the issue can be reproduced
-1. Acknowledge the issue to the researcher that disclosed it
-1. Inform the release manager that there needs to be a security release
-1. Do the steps from [patch release document](../release/patch.md), starting with "Create an issue on private GitLab development server"
-1. The MR with the security fix should get a 'security' label and be assigned to the release manager
-1. Build the package for GitLab.com and do a deploy
-1. Build the package for ci.gitLab.com and do a deploy
-1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
-1. Create feature branches for the blog post on GitLab.com and link them from the code branch
-1. Merge and publish the blog posts
-1. Send tweets about the release from `@gitlabhq`
-1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq)
-1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of.
-1. Add the security researcher to the [Security Researcher Acknowledgments list](https://about.gitlab.com/vulnerability-acknowledgements/)
-1. Thank the security researcher in an email for their cooperation
-1. Update the blog post and the CHANGELOG when we receive the CVE number
-
-The timing of the code merge into master should be coordinated in advance.
-
-After the merge we strive to publish the announcements within 60 minutes.
-
-## Blog post template
-
-XXX Security Advisory for GitLab
-
-A recently discovered critical vulnerability in GitLab allows [unauthenticated API access|remote code execution|unauthorized access to repositories|XXX|PICKSOMETHING]. All users should update GitLab and gitlab-shell immediately. We [have|haven't|XXX|PICKSOMETHING|] heard of this vulnerability being actively exploited.
-
-### Version affected
-
-GitLab Community Edition XXX and lower
-
-GitLab Enterprise Edition XXX and lower
-
-### Fixed versions
-
-GitLab Community Edition XXX and up
-
-GitLab Enterprise Edition XXX and up
-
-### Impact
-
-On GitLab installations which use MySQL as their database backend it is possible for an attacker to assume the identity of any existing GitLab user in certain API calls. This attack can be performed by [unauthenticated|authenticated|XXX|PICKSOMETHING] users.
-
-### Workarounds
-
-If you are unable to upgrade you should apply the following patch and restart GitLab.
-
-XXX
-
-### Credit
-
-We want to thank XXX of XXX for the responsible disclosure of this vulnerability.
-
-## Email template
-
-We just announced a security advisory for GitLab at XXX
-
-Please contact us at support@gitlab.com if you have any questions.
-
-## Tweet template
-
-We just announced a security advisory for GitLab at XXX
diff --git a/doc/update/README.md b/doc/update/README.md
index 109d5de3fa2e55ceb3c3d0d9fdac639204ab06ea..0241f0368306f2abca2910c79cb6ca326e4a685f 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -15,3 +15,4 @@ Depending on the installation method and your GitLab version, there are multiple
 
 - [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating your database from MySQL to PostgreSQL.
 - [MySQL installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/database_mysql.md) contains additional information about configuring GitLab to work with a MySQL database.
+- [Restoring from backup after a failed upgrade](restore_after_failure.md)
diff --git a/doc/update/restore_after_failure.md b/doc/update/restore_after_failure.md
new file mode 100644
index 0000000000000000000000000000000000000000..01c52aae7f56fa682a96e9cbab6ede310e3120fb
--- /dev/null
+++ b/doc/update/restore_after_failure.md
@@ -0,0 +1,83 @@
+# Restoring from backup after a failed upgrade
+
+Upgrades are usually smooth and restoring from backup is a rare occurrence.
+However, it's important to know how to recover when problems do arise.
+
+## Roll back to an earlier version and restore a backup
+
+In some cases after a failed upgrade, the fastest solution is to roll back to
+the previous version you were using.
+
+First, roll back the code or package. For source installations this involves
+checking out the older version (branch or tag). For Omnibus installations this
+means installing the older .deb or .rpm package. Then, restore from a backup.
+Follow the instructions in the
+[Backup and Restore](../raketasks/backup_restore.md#restore-a-previously-created-backup)
+documentation.
+
+## Potential problems on the next upgrade
+
+When a rollback is necessary it can produce problems on subsequent upgrade
+attempts. This is because some tables may have been added during the failed
+upgrade. If these tables are still present after you restore from the
+older backup it can lead to migration failures on future upgrades.
+
+Starting in GitLab 8.6 we drop all tables prior to importing the backup to
+prevent this problem. If you've restored a backup to a version prior to 8.6 you
+may need to manually correct the problem next time you upgrade.
+
+Example error:
+
+```
+== 20151103134857 CreateLfsObjects: migrating =================================
+-- create_table(:lfs_objects)
+rake aborted!
+StandardError: An error has occurred, this and all later migrations canceled:
+
+PG::DuplicateTable: ERROR:  relation "lfs_objects" already exists
+```
+
+Copy the version from the error. In this case the version number is
+`20151103134857`.
+
+>**WARNING:** Use the following steps only if you are certain this is what you
+need to do.
+
+### GitLab 8.6+
+
+Pass the version to a database rake task to manually mark the migration as
+complete.
+
+```
+# Source install
+sudo -u git -H bundle exec rake gitlab:db:mark_migration_complete[20151103134857] RAILS_ENV=production
+
+# Omnibus install
+sudo gitlab-rake gitlab:db:mark_migration_complete[20151103134857]
+```
+
+Once the migration is successfully marked, run the rake `db:migrate` task again.
+You will likely have to repeat this process several times until all failed
+migrations are marked complete.
+
+### GitLab < 8.6
+
+```
+# Source install
+sudo -u git -H bundle exec rails console production
+
+# Omnibus install
+sudo gitlab-rails console
+```
+
+At the Rails console, type the following commands:
+
+```
+ActiveRecord::Base.connection.execute("INSERT INTO schema_migrations (version) VALUES('20151103134857')")
+exit
+```
+
+Once the migration is successfully marked, run the rake `db:migrate` task again.
+You will likely have to repeat this process several times until all failed
+migrations are marked complete.
+
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index afdf1a682e2cfa82b78fedf54ab76d4a4e645aa6..22e207b6d321f4ed9320d9e20013e73f313075c9 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -58,13 +58,13 @@ X-Gitlab-Event: Push Hook
     "path_with_namespace":"mike/diaspora",
     "default_branch":"master",
     "homepage":"http://example.com/mike/diaspora",
-    "url":"git@example.com:mike/diasporadiaspora.git",
+    "url":"git@example.com:mike/diaspora.git",
     "ssh_url":"git@example.com:mike/diaspora.git",
     "http_url":"http://example.com/mike/diaspora.git"
   },
   "repository":{
     "name": "Diaspora",
-    "url": "git@example.com:mike/diasporadiaspora.git",
+    "url": "git@example.com:mike/diaspora.git",
     "description": "",
     "homepage": "http://example.com/mike/diaspora",
     "git_http_url":"http://example.com/mike/diaspora.git",
@@ -113,7 +113,6 @@ Triggered when you create (or delete) tags to the repository.
 X-Gitlab-Event: Tag Push Hook
 ```
 
-
 **Request body:**
 
 ```json
@@ -143,7 +142,7 @@ X-Gitlab-Event: Tag Push Hook
     "http_url":"http://example.com/jsmith/example.git"
   },
   "repository":{
-    "name": "jsmith",
+    "name": "Example",
     "url": "ssh://git@example.com/jsmith/example.git",
     "description": "",
     "homepage": "http://example.com/jsmith/example",
@@ -478,7 +477,7 @@ X-Gitlab-Event: Note Hook
   },
   "repository":{
     "name":"diaspora",
-    "url":"git@example.com:mike/diasporadiaspora.git",
+    "url":"git@example.com:mike/diaspora.git",
     "description":"",
     "homepage":"http://example.com/mike/diaspora"
   },
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
new file mode 100644
index 0000000000000000000000000000000000000000..70b35c58be69756c3f3aba873547112fc1a85277
--- /dev/null
+++ b/doc/workflow/award_emoji.md
@@ -0,0 +1,48 @@
+# Award emojis
+
+>**Note:**
+This feature was [introduced][1825] in GitLab 8.2.
+
+When you're collaborating online, you get fewer opportunities for high-fives
+and thumbs-ups. In order to make virtual celebrations easier, you can now vote
+on issues and merge requests using emoji!
+
+![Award emoji](img/award_emoji_select.png)
+
+This makes it much easier to give and receive feedback, without a long comment
+thread. Any comment that contains only the thumbs up or down emojis is
+converted to a vote and depicted in the emoji area.
+
+You can then use that functionality to sort issues and merge requests based on
+popularity.
+
+## Sort issues and merge requests on vote count
+
+>**Note:**
+This feature was [introduced][2871] in GitLab 8.5.
+
+You can quickly sort the issues or merge requests by the number of votes they
+have received. The sort option can be found in the right dropdown menu.
+
+![Votes sort options](img/award_emoji_votes_sort_options.png)
+
+---
+
+Sort by most popular issues/merge requests.
+
+![Votes sort by most popular](img/award_emoji_votes_most_popular.png)
+
+---
+
+Sort by least popular issues/merge requests.
+
+![Votes sort by least popular](img/award_emoji_votes_least_popular.png)
+
+---
+
+The number of upvotes and downvotes is not summed up. That means that an issue
+with 18 upvotes and 5 downvotes is considered more popular than an issue with
+17 upvotes and no downvotes.
+
+[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
+[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
diff --git a/doc/workflow/img/award_emoji_select.png b/doc/workflow/img/award_emoji_select.png
new file mode 100644
index 0000000000000000000000000000000000000000..fffdfedda5d7536fbc083cde4efac8e489bedf52
Binary files /dev/null and b/doc/workflow/img/award_emoji_select.png differ
diff --git a/doc/workflow/img/award_emoji_votes_least_popular.png b/doc/workflow/img/award_emoji_votes_least_popular.png
new file mode 100644
index 0000000000000000000000000000000000000000..2ef5be7154f020d0a6e775abae42d4cb7a961b02
Binary files /dev/null and b/doc/workflow/img/award_emoji_votes_least_popular.png differ
diff --git a/doc/workflow/img/award_emoji_votes_most_popular.png b/doc/workflow/img/award_emoji_votes_most_popular.png
new file mode 100644
index 0000000000000000000000000000000000000000..5b089730d936cff3788200889216eb85ff625654
Binary files /dev/null and b/doc/workflow/img/award_emoji_votes_most_popular.png differ
diff --git a/doc/workflow/img/award_emoji_votes_sort_options.png b/doc/workflow/img/award_emoji_votes_sort_options.png
new file mode 100644
index 0000000000000000000000000000000000000000..9bbf3f82a0bdda442f3e908ebaa4d85a2fd78c8a
Binary files /dev/null and b/doc/workflow/img/award_emoji_votes_sort_options.png differ
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 36cb9da2380489dc31325699e1fdafadf0bc9b5e..9dc1e9b47e36348e3940d2a6deb4f045e1b9db85 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -23,6 +23,10 @@ In `/etc/gitlab/gitlab.rb`:
 
 ```ruby
 gitlab_rails['lfs_enabled'] = false
+
+# Optionally, change the storage path location. Defaults to
+# `#{gitlab_rails['shared_path']}/lfs-objects`. Which evaluates to
+# `/var/opt/gitlab/gitlab-rails/shared/lfs-objects` by default.
 gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
 ```
 
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index f4a56865532559da8682fce5469fc6d539a4e237..93aa77589be53982619b5a68514560dcfb67bfc8 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -43,10 +43,10 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
 
   step 'I click "All" link' do
     find('.js-author-search').click
-    find('.dropdown-menu-user-full-name', match: :first).click
+    find('.dropdown-content a', match: :first).click
 
     find('.js-assignee-search').click
-    find('.dropdown-menu-user-full-name', match: :first).click
+    find('.dropdown-content a', match: :first).click
   end
 
   def should_see(issue)
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index c89e1b5101933cbd11cabb3f186c4226221fcdd0..b7209c14148e5dddb4446e1bd17eef231950ff7b 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -26,8 +26,8 @@ module Ci
       validate!
     end
 
-    def builds_for_stage_and_ref(stage, ref, tag = false)
-      builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag)}
+    def builds_for_stage_and_ref(stage, ref, tag = false, trigger_request = nil)
+      builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag, trigger_request)}
     end
 
     def builds
@@ -242,9 +242,9 @@ module Ci
       stage_index = stages.index(job[:stage])
 
       job[:dependencies].each do |dependency|
-        raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency]
+        raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
 
-        unless stages.index(@jobs[dependency][:stage]) < stage_index
+        unless stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
           raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
         end
       end
@@ -266,29 +266,30 @@ module Ci
       value.in?([true, false])
     end
 
-    def process?(only_params, except_params, ref, tag)
+    def process?(only_params, except_params, ref, tag, trigger_request)
       if only_params.present?
-        return false unless matching?(only_params, ref, tag)
+        return false unless matching?(only_params, ref, tag, trigger_request)
       end
 
       if except_params.present?
-        return false if matching?(except_params, ref, tag)
+        return false if matching?(except_params, ref, tag, trigger_request)
       end
 
       true
     end
 
-    def matching?(patterns, ref, tag)
+    def matching?(patterns, ref, tag, trigger_request)
       patterns.any? do |pattern|
-        match_ref?(pattern, ref, tag)
+        match_ref?(pattern, ref, tag, trigger_request)
       end
     end
 
-    def match_ref?(pattern, ref, tag)
+    def match_ref?(pattern, ref, tag, trigger_request)
       pattern, path = pattern.split('@', 2)
       return false if path && path != self.path
       return true if tag && pattern == 'tags'
       return true if !tag && pattern == 'branches'
+      return true if trigger_request.present? && pattern == 'triggers'
 
       if pattern.first == "/" && pattern.last == "/"
         Regexp.new(pattern[1...-1]) =~ ref
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 2ca21af5bc8de8bdc765c2f97a12ef2ec335398c..d4b6f6d120db2bc78173296fcbc4aa51a380c48d 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -45,12 +45,12 @@ module Gitlab
         note = create_note(reply)
 
         unless note.persisted?
-          message = "The comment could not be created for the following reasons:"
+          msg = "The comment could not be created for the following reasons:"
           note.errors.full_messages.each do |error|
-            message << "\n\n- #{error}"
+            msg << "\n\n- #{error}"
           end
 
-          raise InvalidNoteError, message
+          raise InvalidNoteError, msg
         end
       end
 
@@ -63,13 +63,13 @@ module Gitlab
       end
 
       def reply_key
-        reply_key = nil
+        key = nil
         message.to.each do |address|
-          reply_key = Gitlab::IncomingEmail.key_from_address(address)
-          break if reply_key
+          key = Gitlab::IncomingEmail.key_from_address(address)
+          break if key
         end
 
-        reply_key
+        key
       end
 
       def sent_notification
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 2ef50286b1ddf6d3217eb0a714200a1823595d65..c73eca832d75c119d69a97a2faee74d69f17de99 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -15,6 +15,25 @@ module Gitlab
   # seconds then two overlapping operations may hold a lease for the same
   # key at the same time.
   #
+  # This class has no 'cancel' method. I originally decided against adding
+  # it because it would add complexity and a false sense of security. The
+  # complexity: instead of setting '1' we would have to set a UUID, and to
+  # delete it we would have to execute Lua on the Redis server to only
+  # delete the key if the value was our own UUID. Otherwise there is a
+  # chance that when you intend to cancel your lease you actually delete
+  # someone else's. The false sense of security: you cannot design your
+  # system to rely too much on the lease being cancelled after use because
+  # the calling (Ruby) process may crash or be killed. You _cannot_ count
+  # on begin/ensure blocks to cancel a lease, because the 'ensure' does
+  # not always run. Think of 'kill -9' from the Unicorn master for
+  # instance.
+  # 
+  # If you find that leases are getting in your way, ask yourself: would
+  # it be enough to lower the lease timeout? Another thing that might be
+  # appropriate is to only use a lease for bulk/automated operations, and
+  # to ignore the lease when you get a single 'manual' user request (a
+  # button click).
+  #
   class ExclusiveLease
     def initialize(key, timeout:)
       @key, @timeout = key, timeout
@@ -27,6 +46,8 @@ module Gitlab
       !!redis.set(redis_key, '1', nx: true, ex: @timeout)
     end
 
+    # No #cancel method. See comments above!
+
     private
 
     def redis
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index cb4abe13799f426cac93a62a95a3d770626ec665..402bb338f27a62c28c0b51d48b6bc937d18365cb 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -22,7 +22,7 @@ namespace :gitlab do
     end
 
     # Restore backup of GitLab system
-    desc "GitLab | Restore a previously created backup"
+    desc 'GitLab | Restore a previously created backup'
     task restore: :environment do
       warn_user_is_not_gitlab
       configure_cron_mode
@@ -30,13 +30,31 @@ namespace :gitlab do
       backup = Backup::Manager.new
       backup.unpack
 
-      Rake::Task["gitlab:backup:db:restore"].invoke unless backup.skipped?("db")
-      Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories")
-      Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads")
-      Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds")
-      Rake::Task["gitlab:backup:artifacts:restore"].invoke unless backup.skipped?("artifacts")
-      Rake::Task["gitlab:backup:lfs:restore"].invoke unless backup.skipped?("lfs")
-      Rake::Task["gitlab:shell:setup"].invoke
+      unless backup.skipped?('db')
+        unless ENV['force'] == 'yes'
+          warning = warning = <<-MSG.strip_heredoc
+            Before restoring the database we recommend removing all existing
+            tables to avoid future upgrade problems. Be aware that if you have
+            custom tables in the GitLab database these tables and all data will be
+            removed.
+          MSG
+          ask_to_continue
+          puts 'Removing all tables. Press `Ctrl-C` within 5 seconds to abort'.yellow
+          sleep(5)
+        end
+        # Drop all tables Load the schema to ensure we don't have any newer tables
+        # hanging out from a failed upgrade
+        $progress.puts 'Cleaning the database ... '.blue
+        Rake::Task['gitlab:db:drop_tables'].invoke
+        $progress.puts 'done'.green
+        Rake::Task['gitlab:backup:db:restore'].invoke
+      end
+      Rake::Task['gitlab:backup:repo:restore'].invoke unless backup.skipped?('repositories')
+      Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads')
+      Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds')
+      Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts')
+      Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs')
+      Rake::Task['gitlab:shell:setup'].invoke
 
       backup.cleanup
     end
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
new file mode 100644
index 0000000000000000000000000000000000000000..4921c6e0bcf4ab43151b6543de680c8dc5f99b80
--- /dev/null
+++ b/lib/tasks/gitlab/db.rake
@@ -0,0 +1,35 @@
+namespace :gitlab do
+  namespace :db do
+    desc 'GitLab | Manually insert schema migration version'
+    task :mark_migration_complete, [:version] => :environment do |_, args|
+      unless args[:version]
+        puts "Must specify a migration version as an argument".red
+        exit 1
+      end
+
+      version = args[:version].to_i
+      if version == 0
+        puts "Version '#{args[:version]}' must be a non-zero integer".red
+        exit 1
+      end
+
+      sql = "INSERT INTO schema_migrations (version) VALUES (#{version})"
+      begin
+        ActiveRecord::Base.connection.execute(sql)
+        puts "Successfully marked '#{version}' as complete".green
+      rescue ActiveRecord::RecordNotUnique
+        puts "Migration version '#{version}' is already marked complete".yellow
+      end
+    end
+
+    desc 'Drop all tables'
+    task :drop_tables => :environment do
+      connection = ActiveRecord::Base.connection
+      tables = connection.tables
+      tables.delete 'schema_migrations'
+      # Truncate schema_migrations to ensure migrations re-run
+      connection.execute('TRUNCATE schema_migrations')
+      tables.each { |t| connection.execute("DROP TABLE #{t}") }
+    end
+  end
+end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 938e97298b64e9ba213ec8b7d3b3364f273e3fb8..465531b2b367b45c1703027436ca6e6e002f0587 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -1,10 +1,15 @@
 require 'rails_helper'
 
 describe GroupsController do
-  describe 'GET index' do
+  let(:user) { create(:user) }
+  let(:group) { create(:group) }
+  let(:project) { create(:project, namespace: group) }
+  let!(:group_member) { create(:group_member, group: group, user: user) }
+
+  describe 'GET #index' do
     context 'as a user' do
       it 'redirects to Groups Dashboard' do
-        sign_in(create(:user))
+        sign_in(user)
 
         get :index
 
@@ -20,4 +25,54 @@ describe GroupsController do
       end
     end
   end
+
+  describe 'GET #issues' do
+    let(:issue_1) { create(:issue, project: project) }
+    let(:issue_2) { create(:issue, project: project) }
+
+    before do
+      create_list(:upvote_note, 3, project: project, noteable: issue_2)
+      create_list(:upvote_note, 2, project: project, noteable: issue_1)
+      create_list(:downvote_note, 2, project: project, noteable: issue_2)
+
+      sign_in(user)
+    end
+
+    context 'sorting by votes' do
+      it 'sorts most popular issues' do
+        get :issues, id: group.to_param, sort: 'upvotes_desc'
+        expect(assigns(:issues)).to eq [issue_2, issue_1]
+      end
+
+      it 'sorts least popular issues' do
+        get :issues, id: group.to_param, sort: 'downvotes_desc'
+        expect(assigns(:issues)).to eq [issue_2, issue_1]
+      end
+    end
+  end
+
+  describe 'GET #merge_requests' do
+    let(:merge_request_1) { create(:merge_request, source_project: project) }
+    let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
+
+    before do
+      create_list(:upvote_note, 3, project: project, noteable: merge_request_2)
+      create_list(:upvote_note, 2, project: project, noteable: merge_request_1)
+      create_list(:downvote_note, 2, project: project, noteable: merge_request_2)
+
+      sign_in(user)
+    end
+
+    context 'sorting by votes' do
+      it 'sorts most popular merge requests' do
+        get :merge_requests, id: group.to_param, sort: 'upvotes_desc'
+        expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
+      end
+
+      it 'sorts least popular merge requests' do
+        get :merge_requests, id: group.to_param, sort: 'downvotes_desc'
+        expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1]
+      end
+    end
+  end
 end
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index 5a104ae7c99a193dc9eb7e9744a0f15f143d4769..b14d275f7faecff9f9f52da8c5e25eb35b155eed 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -43,6 +43,28 @@ describe RootController do
         end
       end
 
+      context 'who has customized their dashboard setting for groups' do
+        before do
+          user.update_attribute(:dashboard, 'groups')
+        end
+
+        it 'redirects to their group list' do
+          get :index
+          expect(response).to redirect_to dashboard_groups_path
+        end
+      end
+
+      context 'who has customized their dashboard setting for todos' do
+        before do
+          user.update_attribute(:dashboard, 'todos')
+        end
+
+        it 'redirects to their todo list' do
+          get :index
+          expect(response).to redirect_to dashboard_todos_path
+        end
+      end
+
       context 'who uses the default dashboard setting' do
         it 'renders the default dashboard' do
           get :index
diff --git a/spec/features/dashboard_milestones_spec.rb b/spec/features/dashboard_milestones_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f32fddbc9fadafc0e7ec8aef58a7d343fc7acc1a
--- /dev/null
+++ b/spec/features/dashboard_milestones_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+feature 'Dashboard > Milestones', feature: true do
+  describe 'as anonymous user' do
+    before do
+      visit dashboard_milestones_path
+    end
+
+    it 'is redirected to sign-in page' do
+      expect(current_path).to eq new_user_session_path
+    end
+  end
+
+  describe 'as logged-in user' do
+    let(:user) { create(:user) }
+    let(:project) { create(:empty_project, namespace: user.namespace) }
+    let!(:milestone) { create(:milestone, project: project) }
+    before do
+      project.team << [user, :master]
+      login_with(user)
+      visit dashboard_milestones_path
+    end
+
+    it 'sees milestones' do
+      expect(current_path).to eq dashboard_milestones_path
+      expect(page).to have_content(milestone.title)
+    end
+  end
+end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3eb903a93feb565982b30474ff5d450433cadcf0
--- /dev/null
+++ b/spec/features/issues/update_issues_spec.rb
@@ -0,0 +1,117 @@
+require 'rails_helper'
+
+feature 'Multiple issue updating from issues#index', feature: true do
+  let!(:project)   { create(:project) }
+  let!(:issue)     { create(:issue, project: project) }
+  let!(:user)      { create(:user)}
+
+  before do
+    project.team << [user, :master]
+    login_as(user)
+  end
+
+  context 'status', js: true do
+    it 'should be set to closed' do
+      visit namespace_project_issues_path(project.namespace, project)
+
+      find('#check_all_issues').click
+      find('.js-issue-status').click
+
+      find('.dropdown-menu-status a', text: 'Closed').click
+      click_update_issues_button
+      expect(page).to have_selector('.issue', count: 0)
+    end
+
+    it 'should be set to open' do
+      create_closed
+      visit namespace_project_issues_path(project.namespace, project)
+
+      find('.issues-state-filters a', text: 'Closed').click
+
+      find('#check_all_issues').click
+      find('.js-issue-status').click
+
+      find('.dropdown-menu-status a', text: 'Open').click
+      click_update_issues_button
+      expect(page).to have_selector('.issue', count: 0)
+    end
+  end
+
+  context 'assignee', js: true do
+    it 'should update to current user' do
+      visit namespace_project_issues_path(project.namespace, project)
+
+      find('#check_all_issues').click
+      find('.js-update-assignee').click
+
+      find('.dropdown-menu-user-link', text: user.username).click
+      click_update_issues_button
+
+      page.within('.issue .controls') do
+        expect(find('.author_link')["data-original-title"]).to have_content(user.name)
+      end
+    end
+
+    it 'should update to unassigned' do
+      create_assigned
+      visit namespace_project_issues_path(project.namespace, project)
+
+      find('#check_all_issues').click
+      find('.js-update-assignee').click
+
+      click_link 'Unassigned'
+      click_update_issues_button
+
+      within first('.issue .controls') do
+        expect(page).to have_no_selector('.author_link')
+      end
+    end
+  end
+
+  context 'milestone', js: true do
+    let(:milestone)  { create(:milestone, project: project) }
+
+    it 'should update milestone' do
+      visit namespace_project_issues_path(project.namespace, project)
+
+      find('#check_all_issues').click
+      find('.issues_bulk_update .js-milestone-select').click
+
+      find('.dropdown-menu-milestone a', text: milestone.title).click
+      click_update_issues_button
+
+      expect(find('.issue')).to have_content milestone.title
+    end
+
+    it 'should set to no milestone' do
+      create_with_milestone
+      visit namespace_project_issues_path(project.namespace, project)
+
+      expect(first('.issue')).to have_content milestone.title
+
+      find('#check_all_issues').click
+      find('.issues_bulk_update .js-milestone-select').click
+
+      find('.dropdown-menu-milestone a', text: "No Milestone").click
+      click_update_issues_button
+
+      expect(first('.issue')).to_not have_content milestone.title
+    end
+  end
+
+  def create_closed
+    create(:issue, project: project, state: :closed)
+  end
+
+  def create_assigned
+    create(:issue, project: project, assignee: user)
+  end
+
+  def create_with_milestone
+    create(:issue, project: project, milestone: milestone)
+  end
+
+  def click_update_issues_button
+    find('.update_selected_issues').click
+  end
+end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index e5df59c4fbaddf6eb3cee252ce12715763d0dc60..2f9291afc3fb4906043c1cb8f2cc11d52b35e281 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -19,7 +19,9 @@ describe PreferencesHelper do
         ['Your Projects (default)', 'projects'],
         ['Starred Projects',        'stars'],
         ["Your Projects' Activity", 'project_activity'],
-        ["Starred Projects' Activity", 'starred_project_activity']
+        ["Starred Projects' Activity", 'starred_project_activity'],
+        ["Your Groups", 'groups'],
+        ["Your Todos", 'todos']
       ]
     end
   end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index fab6412d29fad1d48f17ddd3d60913a1ca42fcea..dcb8a3451bd10d068e805de09192ac185c3e4c42 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -97,6 +97,28 @@ module Ci
           expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
         end
 
+        it "returns builds if only has a triggers keyword specified and a trigger is provided" do
+          config = YAML.dump({
+                               before_script: ["pwd"],
+                               rspec: { script: "rspec", type: type, only: ["triggers"] }
+                             })
+
+          config_processor = GitlabCiYamlProcessor.new(config, path)
+
+          expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, true).size).to eq(1)
+        end
+
+        it "does not return builds if only has a triggers keyword specified and no trigger is provided" do
+          config = YAML.dump({
+                               before_script: ["pwd"],
+                               rspec: { script: "rspec", type: type, only: ["triggers"] }
+                             })
+
+          config_processor = GitlabCiYamlProcessor.new(config, path)
+
+          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+        end
+
         it "returns builds if only has current repository path" do
           config = YAML.dump({
                                before_script: ["pwd"],
@@ -203,6 +225,28 @@ module Ci
           expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
         end
 
+        it "does not return builds if except has a triggers keyword specified and a trigger is provided" do
+          config = YAML.dump({
+                               before_script: ["pwd"],
+                               rspec: { script: "rspec", type: type, except: ["triggers"] }
+                             })
+
+          config_processor = GitlabCiYamlProcessor.new(config, path)
+
+          expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, true).size).to eq(0)
+        end
+
+        it "returns builds if except has a triggers keyword specified and no trigger is provided" do
+          config = YAML.dump({
+                               before_script: ["pwd"],
+                               rspec: { script: "rspec", type: type, except: ["triggers"] }
+                             })
+
+          config_processor = GitlabCiYamlProcessor.new(config, path)
+
+          expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+        end
+
         it "does not return builds if except has current repository path" do
           config = YAML.dump({
                                before_script: ["pwd"],
@@ -448,19 +492,25 @@ module Ci
       end
 
       context 'dependencies to builds' do
+        let(:dependencies) { ['build1', 'build2'] }
+
+        it { expect { subject }.to_not raise_error }
+      end
+
+      context 'dependencies to builds defined as symbols' do
         let(:dependencies) { [:build1, :build2] }
 
         it { expect { subject }.to_not raise_error }
       end
 
       context 'undefined dependency' do
-        let(:dependencies) { [:undefined] }
+        let(:dependencies) { ['undefined'] }
 
         it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
       end
 
       context 'dependencies to deploy' do
-        let(:dependencies) { [:deploy] }
+        let(:dependencies) { ['deploy'] }
 
         it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
       end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 5fe442467385f9f75f0ab24dd669e92ad429ff18..89909c2bcd7ee54d096678a988e108eb59233ae2 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -59,44 +59,70 @@ describe Event, models: true do
     end
 
     it { expect(@event.push?).to be_truthy }
-    it { expect(@event.proper?).to be_truthy }
+    it { expect(@event.visible_to_user?).to be_truthy }
     it { expect(@event.tag?).to be_falsey }
     it { expect(@event.branch_name).to eq("master") }
     it { expect(@event.author).to eq(@user) }
   end
 
-  describe '#proper?' do
-    context 'issue event' do
-      let(:project) { create(:empty_project, :public) }
-      let(:non_member) { create(:user) }
-      let(:member)  { create(:user) }
-      let(:author) { create(:author) }
-      let(:assignee) { create(:user) }
-      let(:admin) { create(:admin) }
-      let(:event) { Event.new(project: project, action: Event::CREATED, target: issue, author_id: author.id) }
-
-      before do
-        project.team << [member, :developer]
-      end
+  describe '#visible_to_user?' do
+    let(:project) { create(:empty_project, :public) }
+    let(:non_member) { create(:user) }
+    let(:member)  { create(:user) }
+    let(:author) { create(:author) }
+    let(:assignee) { create(:user) }
+    let(:admin) { create(:admin) }
+    let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
+    let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+    let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
+    let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
+    let(:event) { Event.new(project: project, target: target, author_id: author.id) }
 
+    before do
+      project.team << [member, :developer]
+    end
+
+    context 'issue event' do
       context 'for non confidential issues' do
-        let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
+        let(:target) { issue }
 
-        it { expect(event.proper?(non_member)).to eq true }
-        it { expect(event.proper?(author)).to eq true }
-        it { expect(event.proper?(assignee)).to eq true }
-        it { expect(event.proper?(member)).to eq true }
-        it { expect(event.proper?(admin)).to eq true }
+        it { expect(event.visible_to_user?(non_member)).to eq true }
+        it { expect(event.visible_to_user?(author)).to eq true }
+        it { expect(event.visible_to_user?(assignee)).to eq true }
+        it { expect(event.visible_to_user?(member)).to eq true }
+        it { expect(event.visible_to_user?(admin)).to eq true }
       end
 
       context 'for confidential issues' do
-        let(:issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+        let(:target) { confidential_issue }
+
+        it { expect(event.visible_to_user?(non_member)).to eq false }
+        it { expect(event.visible_to_user?(author)).to eq true }
+        it { expect(event.visible_to_user?(assignee)).to eq true }
+        it { expect(event.visible_to_user?(member)).to eq true }
+        it { expect(event.visible_to_user?(admin)).to eq true }
+      end
+    end
+
+    context 'note event' do
+      context 'on non confidential issues' do
+        let(:target) { note_on_issue }
+
+        it { expect(event.visible_to_user?(non_member)).to eq true }
+        it { expect(event.visible_to_user?(author)).to eq true }
+        it { expect(event.visible_to_user?(assignee)).to eq true }
+        it { expect(event.visible_to_user?(member)).to eq true }
+        it { expect(event.visible_to_user?(admin)).to eq true }
+      end
+
+      context 'on confidential issues' do
+        let(:target) { note_on_confidential_issue }
 
-        it { expect(event.proper?(non_member)).to eq false }
-        it { expect(event.proper?(author)).to eq true }
-        it { expect(event.proper?(assignee)).to eq true }
-        it { expect(event.proper?(member)).to eq true }
-        it { expect(event.proper?(admin)).to eq true }
+        it { expect(event.visible_to_user?(non_member)).to eq false }
+        it { expect(event.visible_to_user?(author)).to eq true }
+        it { expect(event.visible_to_user?(assignee)).to eq true }
+        it { expect(event.visible_to_user?(member)).to eq true }
+        it { expect(event.visible_to_user?(admin)).to eq true }
       end
     end
   end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 3c34b1d397f4a3981ba8fcbfd4617a973e1aced5..15052aaca28ed42db4b3765a89e4c9d572636560 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -152,6 +152,11 @@ describe Issue, models: true do
 
       it { is_expected.to eq true }
 
+      context 'issue not persisted' do
+        let(:issue) { build(:issue, project: project) }
+        it { is_expected.to eq false }
+      end
+
       context 'checking destination project also' do
         subject { issue.can_move?(user, to_project) }
         let(:to_project) { create(:project) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 20f06f4b7e1412cc8d643ca23b566e249dc621d2..55f1c665b869ee8ba2c35f0efd318f182d153483 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -422,6 +422,12 @@ describe Project, models: true do
 
       it { should eq "http://localhost#{avatar_path}" }
     end
+
+    context 'when git repo is empty' do
+      let(:project) { create(:empty_project) }
+
+      it { should eq nil }
+    end
   end
 
   describe :ci_commit do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 7eac70ae9480a6fd791b07d1a5aaae7f75253188..f10d671104c0fa5dff8960263cf0d60c1f9a2c0c 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
 
 describe Repository, models: true do
   include RepoHelpers
+  TestBlob = Struct.new(:name)
 
   let(:repository) { create(:project).repository }
   let(:user) { create(:user) }
@@ -131,7 +132,6 @@ describe Repository, models: true do
   describe "#license" do
     before do
       repository.send(:cache).expire(:license)
-      TestBlob = Struct.new(:name)
     end
 
     it 'test selection preference' do
@@ -148,6 +148,25 @@ describe Repository, models: true do
     end
   end
 
+  describe "#gitlab_ci_yml" do
+    it 'returns valid file' do
+      files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
+      expect(repository.tree).to receive(:blobs).and_return(files)
+
+      expect(repository.gitlab_ci_yml.name).to eq('.gitlab-ci.yml')
+    end
+
+    it 'returns nil if not exists' do
+      expect(repository.tree).to receive(:blobs).and_return([])
+      expect(repository.gitlab_ci_yml).to be_nil
+    end
+
+    it 'returns nil for empty repository' do
+      expect(repository).to receive(:empty?).and_return(true)
+      expect(repository.gitlab_ci_yml).to be_nil
+    end
+  end
+
   describe :add_branch do
     context 'when pre hooks were successful' do
       it 'should run without errors' do
@@ -725,6 +744,12 @@ describe Repository, models: true do
   end
 
   describe '#avatar' do
+    it 'returns nil if repo does not exist' do
+      expect(repository).to receive(:exists?).and_return(false)
+
+      expect(repository.avatar).to eq(nil)
+    end
+
     it 'returns the first avatar file found in the repository' do
       expect(repository).to receive(:blob_at_branch).
         with('master', 'logo.png').
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 14cc20e529ab1791af7be5ff5f69f96390fdeedb..9b0c73aaf3764662e31f4dd459cecef507a6214c 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -85,6 +85,10 @@ describe Issues::MoveService, services: true do
           expect(old_issue.moved?).to eq true
           expect(old_issue.moved_to).to eq new_issue
         end
+
+        it 'preserves create time' do
+          expect(old_issue.created_at).to eq new_issue.created_at
+        end
       end
 
       context 'issue with notes' do
@@ -121,10 +125,23 @@ describe Issues::MoveService, services: true do
           it 'preserves orignal author of comment' do
             expect(user_notes.pluck(:author_id)).to all(eq(author.id))
           end
+        end
+
+        context 'note that has been updated' do
+          let!(:note) do
+            create(:note, noteable: old_issue, project: old_project,
+                          author: author, updated_at: Date.yesterday,
+                          created_at: Date.yesterday)
+          end
+
+          include_context 'issue move executed'
 
           it 'preserves time when note has been created at' do
-            expect(old_issue.notes.first.created_at)
-              .to eq new_issue.notes.first.created_at
+            expect(new_issue.notes.first.created_at).to eq note.created_at
+          end
+
+          it 'preserves time when note has been updated at' do
+            expect(new_issue.notes.first.updated_at).to eq note.updated_at
           end
         end
 
@@ -208,6 +225,12 @@ describe Issues::MoveService, services: true do
 
         it { expect { move }.to raise_error(StandardError, /permissions/) }
       end
+
+      context 'issue is not persisted' do
+        include_context 'user can move issue'
+        let(:old_issue) { build(:issue, project: old_project, author: author) }
+        it { expect { move }.to raise_error(StandardError, /permissions/) }
+      end
     end
   end
 end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 4ffe753fef57d60167a1545eb3ed59777c658688..6b214a0d96bbf592979542639bf440af2ce16663 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -151,7 +151,12 @@ describe Issues::UpdateService, services: true do
 
     context 'when the issue is relabeled' do
       let!(:non_subscriber) { create(:user) }
-      let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } }
+      let!(:subscriber) do
+        create(:user).tap do |u|
+          label.toggle_subscription(u)
+          project.team << [u, :developer]
+        end
+      end
 
       it 'sends notifications for subscribers of newly added labels' do
         opts = { label_ids: [label.id] }
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index b5407397c1d20e3d07fdd1afb8d0ae4c75340d04..0f2aa3ae73ccd959b29abf3b14ee4c8638a60e17 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -111,6 +111,33 @@ describe NotificationService, services: true do
       end
     end
 
+    context 'confidential issue note' do
+      let(:project) { create(:empty_project, :public) }
+      let(:author) { create(:user) }
+      let(:assignee) { create(:user) }
+      let(:non_member) { create(:user) }
+      let(:member) { create(:user) }
+      let(:admin) { create(:admin) }
+      let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+      let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
+
+      it 'filters out users that can not read the issue' do
+        project.team << [member, :developer]
+
+        expect(SentNotification).to receive(:record).with(confidential_issue, any_args).exactly(4).times
+
+        ActionMailer::Base.deliveries.clear
+
+        notification.new_note(note)
+
+        should_not_email(non_member)
+        should_email(author)
+        should_email(assignee)
+        should_email(member)
+        should_email(admin)
+      end
+    end
+
     context 'issue note mention' do
       let(:project) { create(:empty_project, :public) }
       let(:issue) { create(:issue, project: project, assignee: create(:user)) }
@@ -233,6 +260,36 @@ describe NotificationService, services: true do
 
         should_email(subscriber)
       end
+
+      context 'confidential issues' do
+        let(:author) { create(:user) }
+        let(:assignee) { create(:user) }
+        let(:non_member) { create(:user) }
+        let(:member) { create(:user) }
+        let(:admin) { create(:admin) }
+        let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+
+        it "emails subscribers of the issue's labels that can read the issue" do
+          project.team << [member, :developer]
+
+          label = create(:label, issues: [confidential_issue])
+          label.toggle_subscription(non_member)
+          label.toggle_subscription(author)
+          label.toggle_subscription(assignee)
+          label.toggle_subscription(member)
+          label.toggle_subscription(admin)
+
+          ActionMailer::Base.deliveries.clear
+
+          notification.new_issue(confidential_issue, @u_disabled)
+
+          should_not_email(non_member)
+          should_not_email(author)
+          should_email(assignee)
+          should_email(member)
+          should_email(admin)
+        end
+      end
     end
 
     describe :reassigned_issue do
@@ -332,6 +389,37 @@ describe NotificationService, services: true do
         should_not_email(subscriber_to_label)
         should_email(subscriber_to_label2)
       end
+
+      context 'confidential issues' do
+        let(:author) { create(:user) }
+        let(:assignee) { create(:user) }
+        let(:non_member) { create(:user) }
+        let(:member) { create(:user) }
+        let(:admin) { create(:admin) }
+        let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) }
+        let!(:label_1) { create(:label, issues: [confidential_issue]) }
+        let!(:label_2) { create(:label) }
+
+        it "emails subscribers of the issue's labels that can read the issue" do
+          project.team << [member, :developer]
+
+          label_2.toggle_subscription(non_member)
+          label_2.toggle_subscription(author)
+          label_2.toggle_subscription(assignee)
+          label_2.toggle_subscription(member)
+          label_2.toggle_subscription(admin)
+
+          ActionMailer::Base.deliveries.clear
+
+          notification.relabeled_issue(confidential_issue, [label_2], @u_disabled)
+
+          should_not_email(non_member)
+          should_email(author)
+          should_email(assignee)
+          should_email(member)
+          should_email(admin)
+        end
+      end
     end
 
     describe :close_issue do
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 63bed2414dfbcfac332e615889ecaa753ec6328c..320be9a0b61abb5fd68ce213071c49d53ea98bed 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -3,9 +3,10 @@ require 'rake'
 
 describe 'gitlab:app namespace rake task' do
   before :all do
-    Rake.application.rake_require "tasks/gitlab/task_helpers"
-    Rake.application.rake_require "tasks/gitlab/backup"
-    Rake.application.rake_require "tasks/gitlab/shell"
+    Rake.application.rake_require 'tasks/gitlab/task_helpers'
+    Rake.application.rake_require 'tasks/gitlab/backup'
+    Rake.application.rake_require 'tasks/gitlab/shell'
+    Rake.application.rake_require 'tasks/gitlab/db'
     # empty task as env is already loaded
     Rake::Task.define_task :environment
   end
@@ -37,6 +38,7 @@ describe 'gitlab:app namespace rake task' do
         allow(FileUtils).to receive(:mv).and_return(true)
         allow(Rake::Task["gitlab:shell:setup"]).
           to receive(:invoke).and_return(true)
+        ENV['force'] = 'yes'
       end
 
       let(:gitlab_version) { Gitlab::VERSION }
@@ -52,13 +54,14 @@ describe 'gitlab:app namespace rake task' do
       it 'should invoke restoration on match' do
         allow(YAML).to receive(:load_file).
           and_return({ gitlab_version: gitlab_version })
-        expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke)
-        expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke)
-        expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke)
-        expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke)
-        expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive(:invoke)
-        expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive(:invoke)
-        expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke)
+        expect(Rake::Task['gitlab:db:drop_tables']).to receive(:invoke)
+        expect(Rake::Task['gitlab:backup:db:restore']).to receive(:invoke)
+        expect(Rake::Task['gitlab:backup:repo:restore']).to receive(:invoke)
+        expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke)
+        expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke)
+        expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke)
+        expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke)
+        expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
         expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
       end
     end
@@ -177,17 +180,18 @@ describe 'gitlab:app namespace rake task' do
     end
 
     it 'does not invoke repositories restore' do
-      allow(Rake::Task["gitlab:shell:setup"]).
+      allow(Rake::Task['gitlab:shell:setup']).
         to receive(:invoke).and_return(true)
       allow($stdout).to receive :write
 
-      expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke
-      expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke
-      expect(Rake::Task["gitlab:backup:uploads:restore"]).not_to receive :invoke
-      expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke
-      expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive :invoke
-      expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive :invoke
-      expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
+      expect(Rake::Task['gitlab:db:drop_tables']).to receive :invoke
+      expect(Rake::Task['gitlab:backup:db:restore']).to receive :invoke
+      expect(Rake::Task['gitlab:backup:repo:restore']).not_to receive :invoke
+      expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke
+      expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke
+      expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke
+      expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke
+      expect(Rake::Task['gitlab:shell:setup']).to receive :invoke
       expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
     end
   end
diff --git a/vendor/assets/javascripts/cropper.js b/vendor/assets/javascripts/cropper.js
new file mode 100644
index 0000000000000000000000000000000000000000..805485904a5b98a4ff75b5d06e7331b9570eb5c7
--- /dev/null
+++ b/vendor/assets/javascripts/cropper.js
@@ -0,0 +1,2993 @@
+/*!
+ * Cropper v2.3.0
+ * https://github.com/fengyuanchen/cropper
+ *
+ * Copyright (c) 2014-2016 Fengyuan Chen and contributors
+ * Released under the MIT license
+ *
+ * Date: 2016-02-22T02:13:13.332Z
+ */
+
+(function (factory) {
+  if (typeof define === 'function' && define.amd) {
+    // AMD. Register as anonymous module.
+    define(['jquery'], factory);
+  } else if (typeof exports === 'object') {
+    // Node / CommonJS
+    factory(require('jquery'));
+  } else {
+    // Browser globals.
+    factory(jQuery);
+  }
+})(function ($) {
+
+  'use strict';
+
+  // Globals
+  var $window = $(window);
+  var $document = $(document);
+  var location = window.location;
+  var navigator = window.navigator;
+  var ArrayBuffer = window.ArrayBuffer;
+  var Uint8Array = window.Uint8Array;
+  var DataView = window.DataView;
+  var btoa = window.btoa;
+
+  // Constants
+  var NAMESPACE = 'cropper';
+
+  // Classes
+  var CLASS_MODAL = 'cropper-modal';
+  var CLASS_HIDE = 'cropper-hide';
+  var CLASS_HIDDEN = 'cropper-hidden';
+  var CLASS_INVISIBLE = 'cropper-invisible';
+  var CLASS_MOVE = 'cropper-move';
+  var CLASS_CROP = 'cropper-crop';
+  var CLASS_DISABLED = 'cropper-disabled';
+  var CLASS_BG = 'cropper-bg';
+
+  // Events
+  var EVENT_MOUSE_DOWN = 'mousedown touchstart pointerdown MSPointerDown';
+  var EVENT_MOUSE_MOVE = 'mousemove touchmove pointermove MSPointerMove';
+  var EVENT_MOUSE_UP = 'mouseup touchend touchcancel pointerup pointercancel MSPointerUp MSPointerCancel';
+  var EVENT_WHEEL = 'wheel mousewheel DOMMouseScroll';
+  var EVENT_DBLCLICK = 'dblclick';
+  var EVENT_LOAD = 'load.' + NAMESPACE;
+  var EVENT_ERROR = 'error.' + NAMESPACE;
+  var EVENT_RESIZE = 'resize.' + NAMESPACE; // Bind to window with namespace
+  var EVENT_BUILD = 'build.' + NAMESPACE;
+  var EVENT_BUILT = 'built.' + NAMESPACE;
+  var EVENT_CROP_START = 'cropstart.' + NAMESPACE;
+  var EVENT_CROP_MOVE = 'cropmove.' + NAMESPACE;
+  var EVENT_CROP_END = 'cropend.' + NAMESPACE;
+  var EVENT_CROP = 'crop.' + NAMESPACE;
+  var EVENT_ZOOM = 'zoom.' + NAMESPACE;
+
+  // RegExps
+  var REGEXP_ACTIONS = /e|w|s|n|se|sw|ne|nw|all|crop|move|zoom/;
+  var REGEXP_DATA_URL = /^data\:/;
+  var REGEXP_DATA_URL_HEAD = /^data\:([^\;]+)\;base64,/;
+  var REGEXP_DATA_URL_JPEG = /^data\:image\/jpeg.*;base64,/;
+
+  // Data keys
+  var DATA_PREVIEW = 'preview';
+  var DATA_ACTION = 'action';
+
+  // Actions
+  var ACTION_EAST = 'e';
+  var ACTION_WEST = 'w';
+  var ACTION_SOUTH = 's';
+  var ACTION_NORTH = 'n';
+  var ACTION_SOUTH_EAST = 'se';
+  var ACTION_SOUTH_WEST = 'sw';
+  var ACTION_NORTH_EAST = 'ne';
+  var ACTION_NORTH_WEST = 'nw';
+  var ACTION_ALL = 'all';
+  var ACTION_CROP = 'crop';
+  var ACTION_MOVE = 'move';
+  var ACTION_ZOOM = 'zoom';
+  var ACTION_NONE = 'none';
+
+  // Supports
+  var SUPPORT_CANVAS = $.isFunction($('<canvas>')[0].getContext);
+  var IS_SAFARI = navigator && /safari/i.test(navigator.userAgent) && /apple computer/i.test(navigator.vendor);
+
+  // Maths
+  var num = Number;
+  var min = Math.min;
+  var max = Math.max;
+  var abs = Math.abs;
+  var sin = Math.sin;
+  var cos = Math.cos;
+  var sqrt = Math.sqrt;
+  var round = Math.round;
+  var floor = Math.floor;
+
+  // Utilities
+  var fromCharCode = String.fromCharCode;
+
+  function isNumber(n) {
+    return typeof n === 'number' && !isNaN(n);
+  }
+
+  function isUndefined(n) {
+    return typeof n === 'undefined';
+  }
+
+  function toArray(obj, offset) {
+    var args = [];
+
+    // This is necessary for IE8
+    if (isNumber(offset)) {
+      args.push(offset);
+    }
+
+    return args.slice.apply(obj, args);
+  }
+
+  // Custom proxy to avoid jQuery's guid
+  function proxy(fn, context) {
+    var args = toArray(arguments, 2);
+
+    return function () {
+      return fn.apply(context, args.concat(toArray(arguments)));
+    };
+  }
+
+  function isCrossOriginURL(url) {
+    var parts = url.match(/^(https?:)\/\/([^\:\/\?#]+):?(\d*)/i);
+
+    return parts && (
+      parts[1] !== location.protocol ||
+      parts[2] !== location.hostname ||
+      parts[3] !== location.port
+    );
+  }
+
+  function addTimestamp(url) {
+    var timestamp = 'timestamp=' + (new Date()).getTime();
+
+    return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp);
+  }
+
+  function getCrossOrigin(crossOrigin) {
+    return crossOrigin ? ' crossOrigin="' + crossOrigin + '"' : '';
+  }
+
+  function getImageSize(image, callback) {
+    var newImage;
+
+    // Modern browsers (ignore Safari, #120 & #509)
+    if (image.naturalWidth && !IS_SAFARI) {
+      return callback(image.naturalWidth, image.naturalHeight);
+    }
+
+    // IE8: Don't use `new Image()` here (#319)
+    newImage = document.createElement('img');
+
+    newImage.onload = function () {
+      callback(this.width, this.height);
+    };
+
+    newImage.src = image.src;
+  }
+
+  function getTransform(options) {
+    var transforms = [];
+    var rotate = options.rotate;
+    var scaleX = options.scaleX;
+    var scaleY = options.scaleY;
+
+    if (isNumber(rotate)) {
+      transforms.push('rotate(' + rotate + 'deg)');
+    }
+
+    if (isNumber(scaleX) && isNumber(scaleY)) {
+      transforms.push('scale(' + scaleX + ',' + scaleY + ')');
+    }
+
+    return transforms.length ? transforms.join(' ') : 'none';
+  }
+
+  function getRotatedSizes(data, isReversed) {
+    var deg = abs(data.degree) % 180;
+    var arc = (deg > 90 ? (180 - deg) : deg) * Math.PI / 180;
+    var sinArc = sin(arc);
+    var cosArc = cos(arc);
+    var width = data.width;
+    var height = data.height;
+    var aspectRatio = data.aspectRatio;
+    var newWidth;
+    var newHeight;
+
+    if (!isReversed) {
+      newWidth = width * cosArc + height * sinArc;
+      newHeight = width * sinArc + height * cosArc;
+    } else {
+      newWidth = width / (cosArc + sinArc / aspectRatio);
+      newHeight = newWidth / aspectRatio;
+    }
+
+    return {
+      width: newWidth,
+      height: newHeight
+    };
+  }
+
+  function getSourceCanvas(image, data) {
+    var canvas = $('<canvas>')[0];
+    var context = canvas.getContext('2d');
+    var dstX = 0;
+    var dstY = 0;
+    var dstWidth = data.naturalWidth;
+    var dstHeight = data.naturalHeight;
+    var rotate = data.rotate;
+    var scaleX = data.scaleX;
+    var scaleY = data.scaleY;
+    var scalable = isNumber(scaleX) && isNumber(scaleY) && (scaleX !== 1 || scaleY !== 1);
+    var rotatable = isNumber(rotate) && rotate !== 0;
+    var advanced = rotatable || scalable;
+    var canvasWidth = dstWidth * abs(scaleX || 1);
+    var canvasHeight = dstHeight * abs(scaleY || 1);
+    var translateX;
+    var translateY;
+    var rotated;
+
+    if (scalable) {
+      translateX = canvasWidth / 2;
+      translateY = canvasHeight / 2;
+    }
+
+    if (rotatable) {
+      rotated = getRotatedSizes({
+        width: canvasWidth,
+        height: canvasHeight,
+        degree: rotate
+      });
+
+      canvasWidth = rotated.width;
+      canvasHeight = rotated.height;
+      translateX = canvasWidth / 2;
+      translateY = canvasHeight / 2;
+    }
+
+    canvas.width = canvasWidth;
+    canvas.height = canvasHeight;
+
+    if (advanced) {
+      dstX = -dstWidth / 2;
+      dstY = -dstHeight / 2;
+
+      context.save();
+      context.translate(translateX, translateY);
+    }
+
+    if (rotatable) {
+      context.rotate(rotate * Math.PI / 180);
+    }
+
+    // Should call `scale` after rotated
+    if (scalable) {
+      context.scale(scaleX, scaleY);
+    }
+
+    context.drawImage(image, floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight));
+
+    if (advanced) {
+      context.restore();
+    }
+
+    return canvas;
+  }
+
+  function getTouchesCenter(touches) {
+    var length = touches.length;
+    var pageX = 0;
+    var pageY = 0;
+
+    if (length) {
+      $.each(touches, function (i, touch) {
+        pageX += touch.pageX;
+        pageY += touch.pageY;
+      });
+
+      pageX /= length;
+      pageY /= length;
+    }
+
+    return {
+      pageX: pageX,
+      pageY: pageY
+    };
+  }
+
+  function getStringFromCharCode(dataView, start, length) {
+    var str = '';
+    var i;
+
+    for (i = start, length += start; i < length; i++) {
+      str += fromCharCode(dataView.getUint8(i));
+    }
+
+    return str;
+  }
+
+  function getOrientation(arrayBuffer) {
+    var dataView = new DataView(arrayBuffer);
+    var length = dataView.byteLength;
+    var orientation;
+    var exifIDCode;
+    var tiffOffset;
+    var firstIFDOffset;
+    var littleEndian;
+    var endianness;
+    var app1Start;
+    var ifdStart;
+    var offset;
+    var i;
+
+    // Only handle JPEG image (start by 0xFFD8)
+    if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {
+      offset = 2;
+
+      while (offset < length) {
+        if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
+          app1Start = offset;
+          break;
+        }
+
+        offset++;
+      }
+    }
+
+    if (app1Start) {
+      exifIDCode = app1Start + 4;
+      tiffOffset = app1Start + 10;
+
+      if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') {
+        endianness = dataView.getUint16(tiffOffset);
+        littleEndian = endianness === 0x4949;
+
+        if (littleEndian || endianness === 0x4D4D /* bigEndian */) {
+          if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
+            firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);
+
+            if (firstIFDOffset >= 0x00000008) {
+              ifdStart = tiffOffset + firstIFDOffset;
+            }
+          }
+        }
+      }
+    }
+
+    if (ifdStart) {
+      length = dataView.getUint16(ifdStart, littleEndian);
+
+      for (i = 0; i < length; i++) {
+        offset = ifdStart + i * 12 + 2;
+
+        if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) {
+
+          // 8 is the offset of the current tag's value
+          offset += 8;
+
+          // Get the original orientation value
+          orientation = dataView.getUint16(offset, littleEndian);
+
+          // Override the orientation with its default value for Safari (#120)
+          if (IS_SAFARI) {
+            dataView.setUint16(offset, 1, littleEndian);
+          }
+
+          break;
+        }
+      }
+    }
+
+    return orientation;
+  }
+
+  function dataURLToArrayBuffer(dataURL) {
+    var base64 = dataURL.replace(REGEXP_DATA_URL_HEAD, '');
+    var binary = atob(base64);
+    var length = binary.length;
+    var arrayBuffer = new ArrayBuffer(length);
+    var dataView = new Uint8Array(arrayBuffer);
+    var i;
+
+    for (i = 0; i < length; i++) {
+      dataView[i] = binary.charCodeAt(i);
+    }
+
+    return arrayBuffer;
+  }
+
+  // Only available for JPEG image
+  function arrayBufferToDataURL(arrayBuffer) {
+    var dataView = new Uint8Array(arrayBuffer);
+    var length = dataView.length;
+    var base64 = '';
+    var i;
+
+    for (i = 0; i < length; i++) {
+      base64 += fromCharCode(dataView[i]);
+    }
+
+    return 'data:image/jpeg;base64,' + btoa(base64);
+  }
+
+  function Cropper(element, options) {
+    this.$element = $(element);
+    this.options = $.extend({}, Cropper.DEFAULTS, $.isPlainObject(options) && options);
+    this.isLoaded = false;
+    this.isBuilt = false;
+    this.isCompleted = false;
+    this.isRotated = false;
+    this.isCropped = false;
+    this.isDisabled = false;
+    this.isReplaced = false;
+    this.isLimited = false;
+    this.wheeling = false;
+    this.isImg = false;
+    this.originalUrl = '';
+    this.canvas = null;
+    this.cropBox = null;
+    this.init();
+  }
+
+  Cropper.prototype = {
+    constructor: Cropper,
+
+    init: function () {
+      var $this = this.$element;
+      var url;
+
+      if ($this.is('img')) {
+        this.isImg = true;
+
+        // Should use `$.fn.attr` here. e.g.: "img/picture.jpg"
+        this.originalUrl = url = $this.attr('src');
+
+        // Stop when it's a blank image
+        if (!url) {
+          return;
+        }
+
+        // Should use `$.fn.prop` here. e.g.: "http://example.com/img/picture.jpg"
+        url = $this.prop('src');
+      } else if ($this.is('canvas') && SUPPORT_CANVAS) {
+        url = $this[0].toDataURL();
+      }
+
+      this.load(url);
+    },
+
+    // A shortcut for triggering custom events
+    trigger: function (type, data) {
+      var e = $.Event(type, data);
+
+      this.$element.trigger(e);
+
+      return e;
+    },
+
+    load: function (url) {
+      var options = this.options;
+      var $this = this.$element;
+      var read;
+      var xhr;
+
+      if (!url) {
+        return;
+      }
+
+      // Trigger build event first
+      $this.one(EVENT_BUILD, options.build);
+
+      if (this.trigger(EVENT_BUILD).isDefaultPrevented()) {
+        return;
+      }
+
+      this.url = url;
+      this.image = {};
+
+      if (!options.checkOrientation || !ArrayBuffer) {
+        return this.clone();
+      }
+
+      read = $.proxy(this.read, this);
+
+      // XMLHttpRequest disallows to open a Data URL in some browsers like IE11 and Safari
+      if (REGEXP_DATA_URL.test(url)) {
+        return REGEXP_DATA_URL_JPEG.test(url) ?
+          read(dataURLToArrayBuffer(url)) :
+          this.clone();
+      }
+
+      xhr = new XMLHttpRequest();
+
+      xhr.onerror = xhr.onabort = $.proxy(function () {
+        this.clone();
+      }, this);
+
+      xhr.onload = function () {
+        read(this.response);
+      };
+
+      xhr.open('get', url);
+      xhr.responseType = 'arraybuffer';
+      xhr.send();
+    },
+
+    read: function (arrayBuffer) {
+      var options = this.options;
+      var orientation = getOrientation(arrayBuffer);
+      var image = this.image;
+      var rotate;
+      var scaleX;
+      var scaleY;
+
+      if (orientation > 1) {
+        this.url = arrayBufferToDataURL(arrayBuffer);
+
+        switch (orientation) {
+
+          // flip horizontal
+          case 2:
+            scaleX = -1;
+            break;
+
+          // rotate left 180°
+          case 3:
+            rotate = -180;
+            break;
+
+          // flip vertical
+          case 4:
+            scaleY = -1;
+            break;
+
+          // flip vertical + rotate right 90°
+          case 5:
+            rotate = 90;
+            scaleY = -1;
+            break;
+
+          // rotate right 90°
+          case 6:
+            rotate = 90;
+            break;
+
+          // flip horizontal + rotate right 90°
+          case 7:
+            rotate = 90;
+            scaleX = -1;
+            break;
+
+          // rotate left 90°
+          case 8:
+            rotate = -90;
+            break;
+        }
+      }
+
+      if (options.rotatable) {
+        image.rotate = rotate;
+      }
+
+      if (options.scalable) {
+        image.scaleX = scaleX;
+        image.scaleY = scaleY;
+      }
+
+      this.clone();
+    },
+
+    clone: function () {
+      var options = this.options;
+      var $this = this.$element;
+      var url = this.url;
+      var crossOrigin = '';
+      var crossOriginUrl;
+      var $clone;
+
+      if (options.checkCrossOrigin && isCrossOriginURL(url)) {
+        crossOrigin = $this.prop('crossOrigin');
+
+        if (crossOrigin) {
+          crossOriginUrl = url;
+        } else {
+          crossOrigin = 'anonymous';
+
+          // Bust cache (#148) when there is not a "crossOrigin" property
+          crossOriginUrl = addTimestamp(url);
+        }
+      }
+
+      this.crossOrigin = crossOrigin;
+      this.crossOriginUrl = crossOriginUrl;
+      this.$clone = $clone = $('<img' + getCrossOrigin(crossOrigin) + ' src="' + (crossOriginUrl || url) + '">');
+
+      if (this.isImg) {
+        if ($this[0].complete) {
+          this.start();
+        } else {
+          $this.one(EVENT_LOAD, $.proxy(this.start, this));
+        }
+      } else {
+        $clone.
+          one(EVENT_LOAD, $.proxy(this.start, this)).
+          one(EVENT_ERROR, $.proxy(this.stop, this)).
+          addClass(CLASS_HIDE).
+          insertAfter($this);
+      }
+    },
+
+    start: function () {
+      var $image = this.$element;
+      var $clone = this.$clone;
+
+      if (!this.isImg) {
+        $clone.off(EVENT_ERROR, this.stop);
+        $image = $clone;
+      }
+
+      getImageSize($image[0], $.proxy(function (naturalWidth, naturalHeight) {
+        $.extend(this.image, {
+          naturalWidth: naturalWidth,
+          naturalHeight: naturalHeight,
+          aspectRatio: naturalWidth / naturalHeight
+        });
+
+        this.isLoaded = true;
+        this.build();
+      }, this));
+    },
+
+    stop: function () {
+      this.$clone.remove();
+      this.$clone = null;
+    },
+
+    build: function () {
+      var options = this.options;
+      var $this = this.$element;
+      var $clone = this.$clone;
+      var $cropper;
+      var $cropBox;
+      var $face;
+
+      if (!this.isLoaded) {
+        return;
+      }
+
+      // Unbuild first when replace
+      if (this.isBuilt) {
+        this.unbuild();
+      }
+
+      // Create cropper elements
+      this.$container = $this.parent();
+      this.$cropper = $cropper = $(Cropper.TEMPLATE);
+      this.$canvas = $cropper.find('.cropper-canvas').append($clone);
+      this.$dragBox = $cropper.find('.cropper-drag-box');
+      this.$cropBox = $cropBox = $cropper.find('.cropper-crop-box');
+      this.$viewBox = $cropper.find('.cropper-view-box');
+      this.$face = $face = $cropBox.find('.cropper-face');
+
+      // Hide the original image
+      $this.addClass(CLASS_HIDDEN).after($cropper);
+
+      // Show the clone image if is hidden
+      if (!this.isImg) {
+        $clone.removeClass(CLASS_HIDE);
+      }
+
+      this.initPreview();
+      this.bind();
+
+      options.aspectRatio = max(0, options.aspectRatio) || NaN;
+      options.viewMode = max(0, min(3, round(options.viewMode))) || 0;
+
+      if (options.autoCrop) {
+        this.isCropped = true;
+
+        if (options.modal) {
+          this.$dragBox.addClass(CLASS_MODAL);
+        }
+      } else {
+        $cropBox.addClass(CLASS_HIDDEN);
+      }
+
+      if (!options.guides) {
+        $cropBox.find('.cropper-dashed').addClass(CLASS_HIDDEN);
+      }
+
+      if (!options.center) {
+        $cropBox.find('.cropper-center').addClass(CLASS_HIDDEN);
+      }
+
+      if (options.cropBoxMovable) {
+        $face.addClass(CLASS_MOVE).data(DATA_ACTION, ACTION_ALL);
+      }
+
+      if (!options.highlight) {
+        $face.addClass(CLASS_INVISIBLE);
+      }
+
+      if (options.background) {
+        $cropper.addClass(CLASS_BG);
+      }
+
+      if (!options.cropBoxResizable) {
+        $cropBox.find('.cropper-line, .cropper-point').addClass(CLASS_HIDDEN);
+      }
+
+      this.setDragMode(options.dragMode);
+      this.render();
+      this.isBuilt = true;
+      this.setData(options.data);
+      $this.one(EVENT_BUILT, options.built);
+
+      // Trigger the built event asynchronously to keep `data('cropper')` is defined
+      setTimeout($.proxy(function () {
+        this.trigger(EVENT_BUILT);
+        this.isCompleted = true;
+      }, this), 0);
+    },
+
+    unbuild: function () {
+      if (!this.isBuilt) {
+        return;
+      }
+
+      this.isBuilt = false;
+      this.isCompleted = false;
+      this.initialImage = null;
+
+      // Clear `initialCanvas` is necessary when replace
+      this.initialCanvas = null;
+      this.initialCropBox = null;
+      this.container = null;
+      this.canvas = null;
+
+      // Clear `cropBox` is necessary when replace
+      this.cropBox = null;
+      this.unbind();
+
+      this.resetPreview();
+      this.$preview = null;
+
+      this.$viewBox = null;
+      this.$cropBox = null;
+      this.$dragBox = null;
+      this.$canvas = null;
+      this.$container = null;
+
+      this.$cropper.remove();
+      this.$cropper = null;
+    },
+
+    render: function () {
+      this.initContainer();
+      this.initCanvas();
+      this.initCropBox();
+
+      this.renderCanvas();
+
+      if (this.isCropped) {
+        this.renderCropBox();
+      }
+    },
+
+    initContainer: function () {
+      var options = this.options;
+      var $this = this.$element;
+      var $container = this.$container;
+      var $cropper = this.$cropper;
+
+      $cropper.addClass(CLASS_HIDDEN);
+      $this.removeClass(CLASS_HIDDEN);
+
+      $cropper.css((this.container = {
+        width: max($container.width(), num(options.minContainerWidth) || 200),
+        height: max($container.height(), num(options.minContainerHeight) || 100)
+      }));
+
+      $this.addClass(CLASS_HIDDEN);
+      $cropper.removeClass(CLASS_HIDDEN);
+    },
+
+    // Canvas (image wrapper)
+    initCanvas: function () {
+      var viewMode = this.options.viewMode;
+      var container = this.container;
+      var containerWidth = container.width;
+      var containerHeight = container.height;
+      var image = this.image;
+      var imageNaturalWidth = image.naturalWidth;
+      var imageNaturalHeight = image.naturalHeight;
+      var is90Degree = abs(image.rotate) === 90;
+      var naturalWidth = is90Degree ? imageNaturalHeight : imageNaturalWidth;
+      var naturalHeight = is90Degree ? imageNaturalWidth : imageNaturalHeight;
+      var aspectRatio = naturalWidth / naturalHeight;
+      var canvasWidth = containerWidth;
+      var canvasHeight = containerHeight;
+      var canvas;
+
+      if (containerHeight * aspectRatio > containerWidth) {
+        if (viewMode === 3) {
+          canvasWidth = containerHeight * aspectRatio;
+        } else {
+          canvasHeight = containerWidth / aspectRatio;
+        }
+      } else {
+        if (viewMode === 3) {
+          canvasHeight = containerWidth / aspectRatio;
+        } else {
+          canvasWidth = containerHeight * aspectRatio;
+        }
+      }
+
+      canvas = {
+        naturalWidth: naturalWidth,
+        naturalHeight: naturalHeight,
+        aspectRatio: aspectRatio,
+        width: canvasWidth,
+        height: canvasHeight
+      };
+
+      canvas.oldLeft = canvas.left = (containerWidth - canvasWidth) / 2;
+      canvas.oldTop = canvas.top = (containerHeight - canvasHeight) / 2;
+
+      this.canvas = canvas;
+      this.isLimited = (viewMode === 1 || viewMode === 2);
+      this.limitCanvas(true, true);
+      this.initialImage = $.extend({}, image);
+      this.initialCanvas = $.extend({}, canvas);
+    },
+
+    limitCanvas: function (isSizeLimited, isPositionLimited) {
+      var options = this.options;
+      var viewMode = options.viewMode;
+      var container = this.container;
+      var containerWidth = container.width;
+      var containerHeight = container.height;
+      var canvas = this.canvas;
+      var aspectRatio = canvas.aspectRatio;
+      var cropBox = this.cropBox;
+      var isCropped = this.isCropped && cropBox;
+      var minCanvasWidth;
+      var minCanvasHeight;
+      var newCanvasLeft;
+      var newCanvasTop;
+
+      if (isSizeLimited) {
+        minCanvasWidth = num(options.minCanvasWidth) || 0;
+        minCanvasHeight = num(options.minCanvasHeight) || 0;
+
+        if (viewMode) {
+          if (viewMode > 1) {
+            minCanvasWidth = max(minCanvasWidth, containerWidth);
+            minCanvasHeight = max(minCanvasHeight, containerHeight);
+
+            if (viewMode === 3) {
+              if (minCanvasHeight * aspectRatio > minCanvasWidth) {
+                minCanvasWidth = minCanvasHeight * aspectRatio;
+              } else {
+                minCanvasHeight = minCanvasWidth / aspectRatio;
+              }
+            }
+          } else {
+            if (minCanvasWidth) {
+              minCanvasWidth = max(minCanvasWidth, isCropped ? cropBox.width : 0);
+            } else if (minCanvasHeight) {
+              minCanvasHeight = max(minCanvasHeight, isCropped ? cropBox.height : 0);
+            } else if (isCropped) {
+              minCanvasWidth = cropBox.width;
+              minCanvasHeight = cropBox.height;
+
+              if (minCanvasHeight * aspectRatio > minCanvasWidth) {
+                minCanvasWidth = minCanvasHeight * aspectRatio;
+              } else {
+                minCanvasHeight = minCanvasWidth / aspectRatio;
+              }
+            }
+          }
+        }
+
+        if (minCanvasWidth && minCanvasHeight) {
+          if (minCanvasHeight * aspectRatio > minCanvasWidth) {
+            minCanvasHeight = minCanvasWidth / aspectRatio;
+          } else {
+            minCanvasWidth = minCanvasHeight * aspectRatio;
+          }
+        } else if (minCanvasWidth) {
+          minCanvasHeight = minCanvasWidth / aspectRatio;
+        } else if (minCanvasHeight) {
+          minCanvasWidth = minCanvasHeight * aspectRatio;
+        }
+
+        canvas.minWidth = minCanvasWidth;
+        canvas.minHeight = minCanvasHeight;
+        canvas.maxWidth = Infinity;
+        canvas.maxHeight = Infinity;
+      }
+
+      if (isPositionLimited) {
+        if (viewMode) {
+          newCanvasLeft = containerWidth - canvas.width;
+          newCanvasTop = containerHeight - canvas.height;
+
+          canvas.minLeft = min(0, newCanvasLeft);
+          canvas.minTop = min(0, newCanvasTop);
+          canvas.maxLeft = max(0, newCanvasLeft);
+          canvas.maxTop = max(0, newCanvasTop);
+
+          if (isCropped && this.isLimited) {
+            canvas.minLeft = min(
+              cropBox.left,
+              cropBox.left + cropBox.width - canvas.width
+            );
+            canvas.minTop = min(
+              cropBox.top,
+              cropBox.top + cropBox.height - canvas.height
+            );
+            canvas.maxLeft = cropBox.left;
+            canvas.maxTop = cropBox.top;
+
+            if (viewMode === 2) {
+              if (canvas.width >= containerWidth) {
+                canvas.minLeft = min(0, newCanvasLeft);
+                canvas.maxLeft = max(0, newCanvasLeft);
+              }
+
+              if (canvas.height >= containerHeight) {
+                canvas.minTop = min(0, newCanvasTop);
+                canvas.maxTop = max(0, newCanvasTop);
+              }
+            }
+          }
+        } else {
+          canvas.minLeft = -canvas.width;
+          canvas.minTop = -canvas.height;
+          canvas.maxLeft = containerWidth;
+          canvas.maxTop = containerHeight;
+        }
+      }
+    },
+
+    renderCanvas: function (isChanged) {
+      var canvas = this.canvas;
+      var image = this.image;
+      var rotate = image.rotate;
+      var naturalWidth = image.naturalWidth;
+      var naturalHeight = image.naturalHeight;
+      var aspectRatio;
+      var rotated;
+
+      if (this.isRotated) {
+        this.isRotated = false;
+
+        // Computes rotated sizes with image sizes
+        rotated = getRotatedSizes({
+          width: image.width,
+          height: image.height,
+          degree: rotate
+        });
+
+        aspectRatio = rotated.width / rotated.height;
+
+        if (aspectRatio !== canvas.aspectRatio) {
+          canvas.left -= (rotated.width - canvas.width) / 2;
+          canvas.top -= (rotated.height - canvas.height) / 2;
+          canvas.width = rotated.width;
+          canvas.height = rotated.height;
+          canvas.aspectRatio = aspectRatio;
+          canvas.naturalWidth = naturalWidth;
+          canvas.naturalHeight = naturalHeight;
+
+          // Computes rotated sizes with natural image sizes
+          if (rotate % 180) {
+            rotated = getRotatedSizes({
+              width: naturalWidth,
+              height: naturalHeight,
+              degree: rotate
+            });
+
+            canvas.naturalWidth = rotated.width;
+            canvas.naturalHeight = rotated.height;
+          }
+
+          this.limitCanvas(true, false);
+        }
+      }
+
+      if (canvas.width > canvas.maxWidth || canvas.width < canvas.minWidth) {
+        canvas.left = canvas.oldLeft;
+      }
+
+      if (canvas.height > canvas.maxHeight || canvas.height < canvas.minHeight) {
+        canvas.top = canvas.oldTop;
+      }
+
+      canvas.width = min(max(canvas.width, canvas.minWidth), canvas.maxWidth);
+      canvas.height = min(max(canvas.height, canvas.minHeight), canvas.maxHeight);
+
+      this.limitCanvas(false, true);
+
+      canvas.oldLeft = canvas.left = min(max(canvas.left, canvas.minLeft), canvas.maxLeft);
+      canvas.oldTop = canvas.top = min(max(canvas.top, canvas.minTop), canvas.maxTop);
+
+      this.$canvas.css({
+        width: canvas.width,
+        height: canvas.height,
+        left: canvas.left,
+        top: canvas.top
+      });
+
+      this.renderImage();
+
+      if (this.isCropped && this.isLimited) {
+        this.limitCropBox(true, true);
+      }
+
+      if (isChanged) {
+        this.output();
+      }
+    },
+
+    renderImage: function (isChanged) {
+      var canvas = this.canvas;
+      var image = this.image;
+      var reversed;
+
+      if (image.rotate) {
+        reversed = getRotatedSizes({
+          width: canvas.width,
+          height: canvas.height,
+          degree: image.rotate,
+          aspectRatio: image.aspectRatio
+        }, true);
+      }
+
+      $.extend(image, reversed ? {
+        width: reversed.width,
+        height: reversed.height,
+        left: (canvas.width - reversed.width) / 2,
+        top: (canvas.height - reversed.height) / 2
+      } : {
+        width: canvas.width,
+        height: canvas.height,
+        left: 0,
+        top: 0
+      });
+
+      this.$clone.css({
+        width: image.width,
+        height: image.height,
+        marginLeft: image.left,
+        marginTop: image.top,
+        transform: getTransform(image)
+      });
+
+      if (isChanged) {
+        this.output();
+      }
+    },
+
+    initCropBox: function () {
+      var options = this.options;
+      var canvas = this.canvas;
+      var aspectRatio = options.aspectRatio;
+      var autoCropArea = num(options.autoCropArea) || 0.8;
+      var cropBox = {
+            width: canvas.width,
+            height: canvas.height
+          };
+
+      if (aspectRatio) {
+        if (canvas.height * aspectRatio > canvas.width) {
+          cropBox.height = cropBox.width / aspectRatio;
+        } else {
+          cropBox.width = cropBox.height * aspectRatio;
+        }
+      }
+
+      this.cropBox = cropBox;
+      this.limitCropBox(true, true);
+
+      // Initialize auto crop area
+      cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth);
+      cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight);
+
+      // The width of auto crop area must large than "minWidth", and the height too. (#164)
+      cropBox.width = max(cropBox.minWidth, cropBox.width * autoCropArea);
+      cropBox.height = max(cropBox.minHeight, cropBox.height * autoCropArea);
+      cropBox.oldLeft = cropBox.left = canvas.left + (canvas.width - cropBox.width) / 2;
+      cropBox.oldTop = cropBox.top = canvas.top + (canvas.height - cropBox.height) / 2;
+
+      this.initialCropBox = $.extend({}, cropBox);
+    },
+
+    limitCropBox: function (isSizeLimited, isPositionLimited) {
+      var options = this.options;
+      var aspectRatio = options.aspectRatio;
+      var container = this.container;
+      var containerWidth = container.width;
+      var containerHeight = container.height;
+      var canvas = this.canvas;
+      var cropBox = this.cropBox;
+      var isLimited = this.isLimited;
+      var minCropBoxWidth;
+      var minCropBoxHeight;
+      var maxCropBoxWidth;
+      var maxCropBoxHeight;
+
+      if (isSizeLimited) {
+        minCropBoxWidth = num(options.minCropBoxWidth) || 0;
+        minCropBoxHeight = num(options.minCropBoxHeight) || 0;
+
+        // The min/maxCropBoxWidth/Height must be less than containerWidth/Height
+        minCropBoxWidth = min(minCropBoxWidth, containerWidth);
+        minCropBoxHeight = min(minCropBoxHeight, containerHeight);
+        maxCropBoxWidth = min(containerWidth, isLimited ? canvas.width : containerWidth);
+        maxCropBoxHeight = min(containerHeight, isLimited ? canvas.height : containerHeight);
+
+        if (aspectRatio) {
+          if (minCropBoxWidth && minCropBoxHeight) {
+            if (minCropBoxHeight * aspectRatio > minCropBoxWidth) {
+              minCropBoxHeight = minCropBoxWidth / aspectRatio;
+            } else {
+              minCropBoxWidth = minCropBoxHeight * aspectRatio;
+            }
+          } else if (minCropBoxWidth) {
+            minCropBoxHeight = minCropBoxWidth / aspectRatio;
+          } else if (minCropBoxHeight) {
+            minCropBoxWidth = minCropBoxHeight * aspectRatio;
+          }
+
+          if (maxCropBoxHeight * aspectRatio > maxCropBoxWidth) {
+            maxCropBoxHeight = maxCropBoxWidth / aspectRatio;
+          } else {
+            maxCropBoxWidth = maxCropBoxHeight * aspectRatio;
+          }
+        }
+
+        // The minWidth/Height must be less than maxWidth/Height
+        cropBox.minWidth = min(minCropBoxWidth, maxCropBoxWidth);
+        cropBox.minHeight = min(minCropBoxHeight, maxCropBoxHeight);
+        cropBox.maxWidth = maxCropBoxWidth;
+        cropBox.maxHeight = maxCropBoxHeight;
+      }
+
+      if (isPositionLimited) {
+        if (isLimited) {
+          cropBox.minLeft = max(0, canvas.left);
+          cropBox.minTop = max(0, canvas.top);
+          cropBox.maxLeft = min(containerWidth, canvas.left + canvas.width) - cropBox.width;
+          cropBox.maxTop = min(containerHeight, canvas.top + canvas.height) - cropBox.height;
+        } else {
+          cropBox.minLeft = 0;
+          cropBox.minTop = 0;
+          cropBox.maxLeft = containerWidth - cropBox.width;
+          cropBox.maxTop = containerHeight - cropBox.height;
+        }
+      }
+    },
+
+    renderCropBox: function () {
+      var options = this.options;
+      var container = this.container;
+      var containerWidth = container.width;
+      var containerHeight = container.height;
+      var cropBox = this.cropBox;
+
+      if (cropBox.width > cropBox.maxWidth || cropBox.width < cropBox.minWidth) {
+        cropBox.left = cropBox.oldLeft;
+      }
+
+      if (cropBox.height > cropBox.maxHeight || cropBox.height < cropBox.minHeight) {
+        cropBox.top = cropBox.oldTop;
+      }
+
+      cropBox.width = min(max(cropBox.width, cropBox.minWidth), cropBox.maxWidth);
+      cropBox.height = min(max(cropBox.height, cropBox.minHeight), cropBox.maxHeight);
+
+      this.limitCropBox(false, true);
+
+      cropBox.oldLeft = cropBox.left = min(max(cropBox.left, cropBox.minLeft), cropBox.maxLeft);
+      cropBox.oldTop = cropBox.top = min(max(cropBox.top, cropBox.minTop), cropBox.maxTop);
+
+      if (options.movable && options.cropBoxMovable) {
+
+        // Turn to move the canvas when the crop box is equal to the container
+        this.$face.data(DATA_ACTION, (cropBox.width === containerWidth && cropBox.height === containerHeight) ? ACTION_MOVE : ACTION_ALL);
+      }
+
+      this.$cropBox.css({
+        width: cropBox.width,
+        height: cropBox.height,
+        left: cropBox.left,
+        top: cropBox.top
+      });
+
+      if (this.isCropped && this.isLimited) {
+        this.limitCanvas(true, true);
+      }
+
+      if (!this.isDisabled) {
+        this.output();
+      }
+    },
+
+    output: function () {
+      this.preview();
+
+      if (this.isCompleted) {
+        this.trigger(EVENT_CROP, this.getData());
+      } else if (!this.isBuilt) {
+
+        // Only trigger one crop event before complete
+        this.$element.one(EVENT_BUILT, $.proxy(function () {
+          this.trigger(EVENT_CROP, this.getData());
+        }, this));
+      }
+    },
+
+    initPreview: function () {
+      var crossOrigin = getCrossOrigin(this.crossOrigin);
+      var url = crossOrigin ? this.crossOriginUrl : this.url;
+      var $clone2;
+
+      this.$preview = $(this.options.preview);
+      this.$clone2 = $clone2 = $('<img' + crossOrigin + ' src="' + url + '">');
+      this.$viewBox.html($clone2);
+      this.$preview.each(function () {
+        var $this = $(this);
+
+        // Save the original size for recover
+        $this.data(DATA_PREVIEW, {
+          width: $this.width(),
+          height: $this.height(),
+          html: $this.html()
+        });
+
+        /**
+         * Override img element styles
+         * Add `display:block` to avoid margin top issue
+         * (Occur only when margin-top <= -height)
+         */
+        $this.html(
+          '<img' + crossOrigin + ' src="' + url + '" style="' +
+          'display:block;width:100%;height:auto;' +
+          'min-width:0!important;min-height:0!important;' +
+          'max-width:none!important;max-height:none!important;' +
+          'image-orientation:0deg!important;">'
+        );
+      });
+    },
+
+    resetPreview: function () {
+      this.$preview.each(function () {
+        var $this = $(this);
+        var data = $this.data(DATA_PREVIEW);
+
+        $this.css({
+          width: data.width,
+          height: data.height
+        }).html(data.html).removeData(DATA_PREVIEW);
+      });
+    },
+
+    preview: function () {
+      var image = this.image;
+      var canvas = this.canvas;
+      var cropBox = this.cropBox;
+      var cropBoxWidth = cropBox.width;
+      var cropBoxHeight = cropBox.height;
+      var width = image.width;
+      var height = image.height;
+      var left = cropBox.left - canvas.left - image.left;
+      var top = cropBox.top - canvas.top - image.top;
+
+      if (!this.isCropped || this.isDisabled) {
+        return;
+      }
+
+      this.$clone2.css({
+        width: width,
+        height: height,
+        marginLeft: -left,
+        marginTop: -top,
+        transform: getTransform(image)
+      });
+
+      this.$preview.each(function () {
+        var $this = $(this);
+        var data = $this.data(DATA_PREVIEW);
+        var originalWidth = data.width;
+        var originalHeight = data.height;
+        var newWidth = originalWidth;
+        var newHeight = originalHeight;
+        var ratio = 1;
+
+        if (cropBoxWidth) {
+          ratio = originalWidth / cropBoxWidth;
+          newHeight = cropBoxHeight * ratio;
+        }
+
+        if (cropBoxHeight && newHeight > originalHeight) {
+          ratio = originalHeight / cropBoxHeight;
+          newWidth = cropBoxWidth * ratio;
+          newHeight = originalHeight;
+        }
+
+        $this.css({
+          width: newWidth,
+          height: newHeight
+        }).find('img').css({
+          width: width * ratio,
+          height: height * ratio,
+          marginLeft: -left * ratio,
+          marginTop: -top * ratio,
+          transform: getTransform(image)
+        });
+      });
+    },
+
+    bind: function () {
+      var options = this.options;
+      var $this = this.$element;
+      var $cropper = this.$cropper;
+
+      if ($.isFunction(options.cropstart)) {
+        $this.on(EVENT_CROP_START, options.cropstart);
+      }
+
+      if ($.isFunction(options.cropmove)) {
+        $this.on(EVENT_CROP_MOVE, options.cropmove);
+      }
+
+      if ($.isFunction(options.cropend)) {
+        $this.on(EVENT_CROP_END, options.cropend);
+      }
+
+      if ($.isFunction(options.crop)) {
+        $this.on(EVENT_CROP, options.crop);
+      }
+
+      if ($.isFunction(options.zoom)) {
+        $this.on(EVENT_ZOOM, options.zoom);
+      }
+
+      $cropper.on(EVENT_MOUSE_DOWN, $.proxy(this.cropStart, this));
+
+      if (options.zoomable && options.zoomOnWheel) {
+        $cropper.on(EVENT_WHEEL, $.proxy(this.wheel, this));
+      }
+
+      if (options.toggleDragModeOnDblclick) {
+        $cropper.on(EVENT_DBLCLICK, $.proxy(this.dblclick, this));
+      }
+
+      $document.
+        on(EVENT_MOUSE_MOVE, (this._cropMove = proxy(this.cropMove, this))).
+        on(EVENT_MOUSE_UP, (this._cropEnd = proxy(this.cropEnd, this)));
+
+      if (options.responsive) {
+        $window.on(EVENT_RESIZE, (this._resize = proxy(this.resize, this)));
+      }
+    },
+
+    unbind: function () {
+      var options = this.options;
+      var $this = this.$element;
+      var $cropper = this.$cropper;
+
+      if ($.isFunction(options.cropstart)) {
+        $this.off(EVENT_CROP_START, options.cropstart);
+      }
+
+      if ($.isFunction(options.cropmove)) {
+        $this.off(EVENT_CROP_MOVE, options.cropmove);
+      }
+
+      if ($.isFunction(options.cropend)) {
+        $this.off(EVENT_CROP_END, options.cropend);
+      }
+
+      if ($.isFunction(options.crop)) {
+        $this.off(EVENT_CROP, options.crop);
+      }
+
+      if ($.isFunction(options.zoom)) {
+        $this.off(EVENT_ZOOM, options.zoom);
+      }
+
+      $cropper.off(EVENT_MOUSE_DOWN, this.cropStart);
+
+      if (options.zoomable && options.zoomOnWheel) {
+        $cropper.off(EVENT_WHEEL, this.wheel);
+      }
+
+      if (options.toggleDragModeOnDblclick) {
+        $cropper.off(EVENT_DBLCLICK, this.dblclick);
+      }
+
+      $document.
+        off(EVENT_MOUSE_MOVE, this._cropMove).
+        off(EVENT_MOUSE_UP, this._cropEnd);
+
+      if (options.responsive) {
+        $window.off(EVENT_RESIZE, this._resize);
+      }
+    },
+
+    resize: function () {
+      var restore = this.options.restore;
+      var $container = this.$container;
+      var container = this.container;
+      var canvasData;
+      var cropBoxData;
+      var ratio;
+
+      // Check `container` is necessary for IE8
+      if (this.isDisabled || !container) {
+        return;
+      }
+
+      ratio = $container.width() / container.width;
+
+      // Resize when width changed or height changed
+      if (ratio !== 1 || $container.height() !== container.height) {
+        if (restore) {
+          canvasData = this.getCanvasData();
+          cropBoxData = this.getCropBoxData();
+        }
+
+        this.render();
+
+        if (restore) {
+          this.setCanvasData($.each(canvasData, function (i, n) {
+            canvasData[i] = n * ratio;
+          }));
+          this.setCropBoxData($.each(cropBoxData, function (i, n) {
+            cropBoxData[i] = n * ratio;
+          }));
+        }
+      }
+    },
+
+    dblclick: function () {
+      if (this.isDisabled) {
+        return;
+      }
+
+      if (this.$dragBox.hasClass(CLASS_CROP)) {
+        this.setDragMode(ACTION_MOVE);
+      } else {
+        this.setDragMode(ACTION_CROP);
+      }
+    },
+
+    wheel: function (event) {
+      var e = event.originalEvent || event;
+      var ratio = num(this.options.wheelZoomRatio) || 0.1;
+      var delta = 1;
+
+      if (this.isDisabled) {
+        return;
+      }
+
+      event.preventDefault();
+
+      // Limit wheel speed to prevent zoom too fast
+      if (this.wheeling) {
+        return;
+      }
+
+      this.wheeling = true;
+
+      setTimeout($.proxy(function () {
+        this.wheeling = false;
+      }, this), 50);
+
+      if (e.deltaY) {
+        delta = e.deltaY > 0 ? 1 : -1;
+      } else if (e.wheelDelta) {
+        delta = -e.wheelDelta / 120;
+      } else if (e.detail) {
+        delta = e.detail > 0 ? 1 : -1;
+      }
+
+      this.zoom(-delta * ratio, event);
+    },
+
+    cropStart: function (event) {
+      var options = this.options;
+      var originalEvent = event.originalEvent;
+      var touches = originalEvent && originalEvent.touches;
+      var e = event;
+      var touchesLength;
+      var action;
+
+      if (this.isDisabled) {
+        return;
+      }
+
+      if (touches) {
+        touchesLength = touches.length;
+
+        if (touchesLength > 1) {
+          if (options.zoomable && options.zoomOnTouch && touchesLength === 2) {
+            e = touches[1];
+            this.startX2 = e.pageX;
+            this.startY2 = e.pageY;
+            action = ACTION_ZOOM;
+          } else {
+            return;
+          }
+        }
+
+        e = touches[0];
+      }
+
+      action = action || $(e.target).data(DATA_ACTION);
+
+      if (REGEXP_ACTIONS.test(action)) {
+        if (this.trigger(EVENT_CROP_START, {
+          originalEvent: originalEvent,
+          action: action
+        }).isDefaultPrevented()) {
+          return;
+        }
+
+        event.preventDefault();
+
+        this.action = action;
+        this.cropping = false;
+
+        // IE8  has `event.pageX/Y`, but not `event.originalEvent.pageX/Y`
+        // IE10 has `event.originalEvent.pageX/Y`, but not `event.pageX/Y`
+        this.startX = e.pageX || originalEvent && originalEvent.pageX;
+        this.startY = e.pageY || originalEvent && originalEvent.pageY;
+
+        if (action === ACTION_CROP) {
+          this.cropping = true;
+          this.$dragBox.addClass(CLASS_MODAL);
+        }
+      }
+    },
+
+    cropMove: function (event) {
+      var options = this.options;
+      var originalEvent = event.originalEvent;
+      var touches = originalEvent && originalEvent.touches;
+      var e = event;
+      var action = this.action;
+      var touchesLength;
+
+      if (this.isDisabled) {
+        return;
+      }
+
+      if (touches) {
+        touchesLength = touches.length;
+
+        if (touchesLength > 1) {
+          if (options.zoomable && options.zoomOnTouch && touchesLength === 2) {
+            e = touches[1];
+            this.endX2 = e.pageX;
+            this.endY2 = e.pageY;
+          } else {
+            return;
+          }
+        }
+
+        e = touches[0];
+      }
+
+      if (action) {
+        if (this.trigger(EVENT_CROP_MOVE, {
+          originalEvent: originalEvent,
+          action: action
+        }).isDefaultPrevented()) {
+          return;
+        }
+
+        event.preventDefault();
+
+        this.endX = e.pageX || originalEvent && originalEvent.pageX;
+        this.endY = e.pageY || originalEvent && originalEvent.pageY;
+
+        this.change(e.shiftKey, action === ACTION_ZOOM ? event : null);
+      }
+    },
+
+    cropEnd: function (event) {
+      var originalEvent = event.originalEvent;
+      var action = this.action;
+
+      if (this.isDisabled) {
+        return;
+      }
+
+      if (action) {
+        event.preventDefault();
+
+        if (this.cropping) {
+          this.cropping = false;
+          this.$dragBox.toggleClass(CLASS_MODAL, this.isCropped && this.options.modal);
+        }
+
+        this.action = '';
+
+        this.trigger(EVENT_CROP_END, {
+          originalEvent: originalEvent,
+          action: action
+        });
+      }
+    },
+
+    change: function (shiftKey, event) {
+      var options = this.options;
+      var aspectRatio = options.aspectRatio;
+      var action = this.action;
+      var container = this.container;
+      var canvas = this.canvas;
+      var cropBox = this.cropBox;
+      var width = cropBox.width;
+      var height = cropBox.height;
+      var left = cropBox.left;
+      var top = cropBox.top;
+      var right = left + width;
+      var bottom = top + height;
+      var minLeft = 0;
+      var minTop = 0;
+      var maxWidth = container.width;
+      var maxHeight = container.height;
+      var renderable = true;
+      var offset;
+      var range;
+
+      // Locking aspect ratio in "free mode" by holding shift key (#259)
+      if (!aspectRatio && shiftKey) {
+        aspectRatio = width && height ? width / height : 1;
+      }
+
+      if (this.limited) {
+        minLeft = cropBox.minLeft;
+        minTop = cropBox.minTop;
+        maxWidth = minLeft + min(container.width, canvas.left + canvas.width);
+        maxHeight = minTop + min(container.height, canvas.top + canvas.height);
+      }
+
+      range = {
+        x: this.endX - this.startX,
+        y: this.endY - this.startY
+      };
+
+      if (aspectRatio) {
+        range.X = range.y * aspectRatio;
+        range.Y = range.x / aspectRatio;
+      }
+
+      switch (action) {
+        // Move crop box
+        case ACTION_ALL:
+          left += range.x;
+          top += range.y;
+          break;
+
+        // Resize crop box
+        case ACTION_EAST:
+          if (range.x >= 0 && (right >= maxWidth || aspectRatio &&
+            (top <= minTop || bottom >= maxHeight))) {
+
+            renderable = false;
+            break;
+          }
+
+          width += range.x;
+
+          if (aspectRatio) {
+            height = width / aspectRatio;
+            top -= range.Y / 2;
+          }
+
+          if (width < 0) {
+            action = ACTION_WEST;
+            width = 0;
+          }
+
+          break;
+
+        case ACTION_NORTH:
+          if (range.y <= 0 && (top <= minTop || aspectRatio &&
+            (left <= minLeft || right >= maxWidth))) {
+
+            renderable = false;
+            break;
+          }
+
+          height -= range.y;
+          top += range.y;
+
+          if (aspectRatio) {
+            width = height * aspectRatio;
+            left += range.X / 2;
+          }
+
+          if (height < 0) {
+            action = ACTION_SOUTH;
+            height = 0;
+          }
+
+          break;
+
+        case ACTION_WEST:
+          if (range.x <= 0 && (left <= minLeft || aspectRatio &&
+            (top <= minTop || bottom >= maxHeight))) {
+
+            renderable = false;
+            break;
+          }
+
+          width -= range.x;
+          left += range.x;
+
+          if (aspectRatio) {
+            height = width / aspectRatio;
+            top += range.Y / 2;
+          }
+
+          if (width < 0) {
+            action = ACTION_EAST;
+            width = 0;
+          }
+
+          break;
+
+        case ACTION_SOUTH:
+          if (range.y >= 0 && (bottom >= maxHeight || aspectRatio &&
+            (left <= minLeft || right >= maxWidth))) {
+
+            renderable = false;
+            break;
+          }
+
+          height += range.y;
+
+          if (aspectRatio) {
+            width = height * aspectRatio;
+            left -= range.X / 2;
+          }
+
+          if (height < 0) {
+            action = ACTION_NORTH;
+            height = 0;
+          }
+
+          break;
+
+        case ACTION_NORTH_EAST:
+          if (aspectRatio) {
+            if (range.y <= 0 && (top <= minTop || right >= maxWidth)) {
+              renderable = false;
+              break;
+            }
+
+            height -= range.y;
+            top += range.y;
+            width = height * aspectRatio;
+          } else {
+            if (range.x >= 0) {
+              if (right < maxWidth) {
+                width += range.x;
+              } else if (range.y <= 0 && top <= minTop) {
+                renderable = false;
+              }
+            } else {
+              width += range.x;
+            }
+
+            if (range.y <= 0) {
+              if (top > minTop) {
+                height -= range.y;
+                top += range.y;
+              }
+            } else {
+              height -= range.y;
+              top += range.y;
+            }
+          }
+
+          if (width < 0 && height < 0) {
+            action = ACTION_SOUTH_WEST;
+            height = 0;
+            width = 0;
+          } else if (width < 0) {
+            action = ACTION_NORTH_WEST;
+            width = 0;
+          } else if (height < 0) {
+            action = ACTION_SOUTH_EAST;
+            height = 0;
+          }
+
+          break;
+
+        case ACTION_NORTH_WEST:
+          if (aspectRatio) {
+            if (range.y <= 0 && (top <= minTop || left <= minLeft)) {
+              renderable = false;
+              break;
+            }
+
+            height -= range.y;
+            top += range.y;
+            width = height * aspectRatio;
+            left += range.X;
+          } else {
+            if (range.x <= 0) {
+              if (left > minLeft) {
+                width -= range.x;
+                left += range.x;
+              } else if (range.y <= 0 && top <= minTop) {
+                renderable = false;
+              }
+            } else {
+              width -= range.x;
+              left += range.x;
+            }
+
+            if (range.y <= 0) {
+              if (top > minTop) {
+                height -= range.y;
+                top += range.y;
+              }
+            } else {
+              height -= range.y;
+              top += range.y;
+            }
+          }
+
+          if (width < 0 && height < 0) {
+            action = ACTION_SOUTH_EAST;
+            height = 0;
+            width = 0;
+          } else if (width < 0) {
+            action = ACTION_NORTH_EAST;
+            width = 0;
+          } else if (height < 0) {
+            action = ACTION_SOUTH_WEST;
+            height = 0;
+          }
+
+          break;
+
+        case ACTION_SOUTH_WEST:
+          if (aspectRatio) {
+            if (range.x <= 0 && (left <= minLeft || bottom >= maxHeight)) {
+              renderable = false;
+              break;
+            }
+
+            width -= range.x;
+            left += range.x;
+            height = width / aspectRatio;
+          } else {
+            if (range.x <= 0) {
+              if (left > minLeft) {
+                width -= range.x;
+                left += range.x;
+              } else if (range.y >= 0 && bottom >= maxHeight) {
+                renderable = false;
+              }
+            } else {
+              width -= range.x;
+              left += range.x;
+            }
+
+            if (range.y >= 0) {
+              if (bottom < maxHeight) {
+                height += range.y;
+              }
+            } else {
+              height += range.y;
+            }
+          }
+
+          if (width < 0 && height < 0) {
+            action = ACTION_NORTH_EAST;
+            height = 0;
+            width = 0;
+          } else if (width < 0) {
+            action = ACTION_SOUTH_EAST;
+            width = 0;
+          } else if (height < 0) {
+            action = ACTION_NORTH_WEST;
+            height = 0;
+          }
+
+          break;
+
+        case ACTION_SOUTH_EAST:
+          if (aspectRatio) {
+            if (range.x >= 0 && (right >= maxWidth || bottom >= maxHeight)) {
+              renderable = false;
+              break;
+            }
+
+            width += range.x;
+            height = width / aspectRatio;
+          } else {
+            if (range.x >= 0) {
+              if (right < maxWidth) {
+                width += range.x;
+              } else if (range.y >= 0 && bottom >= maxHeight) {
+                renderable = false;
+              }
+            } else {
+              width += range.x;
+            }
+
+            if (range.y >= 0) {
+              if (bottom < maxHeight) {
+                height += range.y;
+              }
+            } else {
+              height += range.y;
+            }
+          }
+
+          if (width < 0 && height < 0) {
+            action = ACTION_NORTH_WEST;
+            height = 0;
+            width = 0;
+          } else if (width < 0) {
+            action = ACTION_SOUTH_WEST;
+            width = 0;
+          } else if (height < 0) {
+            action = ACTION_NORTH_EAST;
+            height = 0;
+          }
+
+          break;
+
+        // Move canvas
+        case ACTION_MOVE:
+          this.move(range.x, range.y);
+          renderable = false;
+          break;
+
+        // Zoom canvas
+        case ACTION_ZOOM:
+          this.zoom((function (x1, y1, x2, y2) {
+            var z1 = sqrt(x1 * x1 + y1 * y1);
+            var z2 = sqrt(x2 * x2 + y2 * y2);
+
+            return (z2 - z1) / z1;
+          })(
+            abs(this.startX - this.startX2),
+            abs(this.startY - this.startY2),
+            abs(this.endX - this.endX2),
+            abs(this.endY - this.endY2)
+          ), event);
+          this.startX2 = this.endX2;
+          this.startY2 = this.endY2;
+          renderable = false;
+          break;
+
+        // Create crop box
+        case ACTION_CROP:
+          if (!range.x || !range.y) {
+            renderable = false;
+            break;
+          }
+
+          offset = this.$cropper.offset();
+          left = this.startX - offset.left;
+          top = this.startY - offset.top;
+          width = cropBox.minWidth;
+          height = cropBox.minHeight;
+
+          if (range.x > 0) {
+            action = range.y > 0 ? ACTION_SOUTH_EAST : ACTION_NORTH_EAST;
+          } else if (range.x < 0) {
+            left -= width;
+            action = range.y > 0 ? ACTION_SOUTH_WEST : ACTION_NORTH_WEST;
+          }
+
+          if (range.y < 0) {
+            top -= height;
+          }
+
+          // Show the crop box if is hidden
+          if (!this.isCropped) {
+            this.$cropBox.removeClass(CLASS_HIDDEN);
+            this.isCropped = true;
+
+            if (this.limited) {
+              this.limitCropBox(true, true);
+            }
+          }
+
+          break;
+
+        // No default
+      }
+
+      if (renderable) {
+        cropBox.width = width;
+        cropBox.height = height;
+        cropBox.left = left;
+        cropBox.top = top;
+        this.action = action;
+
+        this.renderCropBox();
+      }
+
+      // Override
+      this.startX = this.endX;
+      this.startY = this.endY;
+    },
+
+    // Show the crop box manually
+    crop: function () {
+      if (!this.isBuilt || this.isDisabled) {
+        return;
+      }
+
+      if (!this.isCropped) {
+        this.isCropped = true;
+        this.limitCropBox(true, true);
+
+        if (this.options.modal) {
+          this.$dragBox.addClass(CLASS_MODAL);
+        }
+
+        this.$cropBox.removeClass(CLASS_HIDDEN);
+      }
+
+      this.setCropBoxData(this.initialCropBox);
+    },
+
+    // Reset the image and crop box to their initial states
+    reset: function () {
+      if (!this.isBuilt || this.isDisabled) {
+        return;
+      }
+
+      this.image = $.extend({}, this.initialImage);
+      this.canvas = $.extend({}, this.initialCanvas);
+      this.cropBox = $.extend({}, this.initialCropBox);
+
+      this.renderCanvas();
+
+      if (this.isCropped) {
+        this.renderCropBox();
+      }
+    },
+
+    // Clear the crop box
+    clear: function () {
+      if (!this.isCropped || this.isDisabled) {
+        return;
+      }
+
+      $.extend(this.cropBox, {
+        left: 0,
+        top: 0,
+        width: 0,
+        height: 0
+      });
+
+      this.isCropped = false;
+      this.renderCropBox();
+
+      this.limitCanvas(true, true);
+
+      // Render canvas after crop box rendered
+      this.renderCanvas();
+
+      this.$dragBox.removeClass(CLASS_MODAL);
+      this.$cropBox.addClass(CLASS_HIDDEN);
+    },
+
+    /**
+     * Replace the image's src and rebuild the cropper
+     *
+     * @param {String} url
+     * @param {Boolean} onlyColorChanged (optional)
+     */
+    replace: function (url, onlyColorChanged) {
+      if (!this.isDisabled && url) {
+        if (this.isImg) {
+          this.$element.attr('src', url);
+        }
+
+        if (onlyColorChanged) {
+          this.url = url;
+          this.$clone.attr('src', url);
+
+          if (this.isBuilt) {
+            this.$preview.find('img').add(this.$clone2).attr('src', url);
+          }
+        } else {
+          if (this.isImg) {
+            this.isReplaced = true;
+          }
+
+          // Clear previous data
+          this.options.data = null;
+          this.load(url);
+        }
+      }
+    },
+
+    // Enable (unfreeze) the cropper
+    enable: function () {
+      if (this.isBuilt) {
+        this.isDisabled = false;
+        this.$cropper.removeClass(CLASS_DISABLED);
+      }
+    },
+
+    // Disable (freeze) the cropper
+    disable: function () {
+      if (this.isBuilt) {
+        this.isDisabled = true;
+        this.$cropper.addClass(CLASS_DISABLED);
+      }
+    },
+
+    // Destroy the cropper and remove the instance from the image
+    destroy: function () {
+      var $this = this.$element;
+
+      if (this.isLoaded) {
+        if (this.isImg && this.isReplaced) {
+          $this.attr('src', this.originalUrl);
+        }
+
+        this.unbuild();
+        $this.removeClass(CLASS_HIDDEN);
+      } else {
+        if (this.isImg) {
+          $this.off(EVENT_LOAD, this.start);
+        } else if (this.$clone) {
+          this.$clone.remove();
+        }
+      }
+
+      $this.removeData(NAMESPACE);
+    },
+
+    /**
+     * Move the canvas with relative offsets
+     *
+     * @param {Number} offsetX
+     * @param {Number} offsetY (optional)
+     */
+    move: function (offsetX, offsetY) {
+      var canvas = this.canvas;
+
+      this.moveTo(
+        isUndefined(offsetX) ? offsetX : canvas.left + num(offsetX),
+        isUndefined(offsetY) ? offsetY : canvas.top + num(offsetY)
+      );
+    },
+
+    /**
+     * Move the canvas to an absolute point
+     *
+     * @param {Number} x
+     * @param {Number} y (optional)
+     */
+    moveTo: function (x, y) {
+      var canvas = this.canvas;
+      var isChanged = false;
+
+      // If "y" is not present, its default value is "x"
+      if (isUndefined(y)) {
+        y = x;
+      }
+
+      x = num(x);
+      y = num(y);
+
+      if (this.isBuilt && !this.isDisabled && this.options.movable) {
+        if (isNumber(x)) {
+          canvas.left = x;
+          isChanged = true;
+        }
+
+        if (isNumber(y)) {
+          canvas.top = y;
+          isChanged = true;
+        }
+
+        if (isChanged) {
+          this.renderCanvas(true);
+        }
+      }
+    },
+
+    /**
+     * Zoom the canvas with a relative ratio
+     *
+     * @param {Number} ratio
+     * @param {jQuery Event} _event (private)
+     */
+    zoom: function (ratio, _event) {
+      var canvas = this.canvas;
+
+      ratio = num(ratio);
+
+      if (ratio < 0) {
+        ratio =  1 / (1 - ratio);
+      } else {
+        ratio = 1 + ratio;
+      }
+
+      this.zoomTo(canvas.width * ratio / canvas.naturalWidth, _event);
+    },
+
+    /**
+     * Zoom the canvas to an absolute ratio
+     *
+     * @param {Number} ratio
+     * @param {jQuery Event} _event (private)
+     */
+    zoomTo: function (ratio, _event) {
+      var options = this.options;
+      var canvas = this.canvas;
+      var width = canvas.width;
+      var height = canvas.height;
+      var naturalWidth = canvas.naturalWidth;
+      var naturalHeight = canvas.naturalHeight;
+      var originalEvent;
+      var newWidth;
+      var newHeight;
+      var offset;
+      var center;
+
+      ratio = num(ratio);
+
+      if (ratio >= 0 && this.isBuilt && !this.isDisabled && options.zoomable) {
+        newWidth = naturalWidth * ratio;
+        newHeight = naturalHeight * ratio;
+
+        if (_event) {
+          originalEvent = _event.originalEvent;
+        }
+
+        if (this.trigger(EVENT_ZOOM, {
+          originalEvent: originalEvent,
+          oldRatio: width / naturalWidth,
+          ratio: newWidth / naturalWidth
+        }).isDefaultPrevented()) {
+          return;
+        }
+
+        if (originalEvent) {
+          offset = this.$cropper.offset();
+          center = originalEvent.touches ? getTouchesCenter(originalEvent.touches) : {
+            pageX: _event.pageX || originalEvent.pageX || 0,
+            pageY: _event.pageY || originalEvent.pageY || 0
+          };
+
+          // Zoom from the triggering point of the event
+          canvas.left -= (newWidth - width) * (
+            ((center.pageX - offset.left) - canvas.left) / width
+          );
+          canvas.top -= (newHeight - height) * (
+            ((center.pageY - offset.top) - canvas.top) / height
+          );
+        } else {
+
+          // Zoom from the center of the canvas
+          canvas.left -= (newWidth - width) / 2;
+          canvas.top -= (newHeight - height) / 2;
+        }
+
+        canvas.width = newWidth;
+        canvas.height = newHeight;
+        this.renderCanvas(true);
+      }
+    },
+
+    /**
+     * Rotate the canvas with a relative degree
+     *
+     * @param {Number} degree
+     */
+    rotate: function (degree) {
+      this.rotateTo((this.image.rotate || 0) + num(degree));
+    },
+
+    /**
+     * Rotate the canvas to an absolute degree
+     * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#rotate()
+     *
+     * @param {Number} degree
+     */
+    rotateTo: function (degree) {
+      degree = num(degree);
+
+      if (isNumber(degree) && this.isBuilt && !this.isDisabled && this.options.rotatable) {
+        this.image.rotate = degree % 360;
+        this.isRotated = true;
+        this.renderCanvas(true);
+      }
+    },
+
+    /**
+     * Scale the image
+     * https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function#scale()
+     *
+     * @param {Number} scaleX
+     * @param {Number} scaleY (optional)
+     */
+    scale: function (scaleX, scaleY) {
+      var image = this.image;
+      var isChanged = false;
+
+      // If "scaleY" is not present, its default value is "scaleX"
+      if (isUndefined(scaleY)) {
+        scaleY = scaleX;
+      }
+
+      scaleX = num(scaleX);
+      scaleY = num(scaleY);
+
+      if (this.isBuilt && !this.isDisabled && this.options.scalable) {
+        if (isNumber(scaleX)) {
+          image.scaleX = scaleX;
+          isChanged = true;
+        }
+
+        if (isNumber(scaleY)) {
+          image.scaleY = scaleY;
+          isChanged = true;
+        }
+
+        if (isChanged) {
+          this.renderImage(true);
+        }
+      }
+    },
+
+    /**
+     * Scale the abscissa of the image
+     *
+     * @param {Number} scaleX
+     */
+    scaleX: function (scaleX) {
+      var scaleY = this.image.scaleY;
+
+      this.scale(scaleX, isNumber(scaleY) ? scaleY : 1);
+    },
+
+    /**
+     * Scale the ordinate of the image
+     *
+     * @param {Number} scaleY
+     */
+    scaleY: function (scaleY) {
+      var scaleX = this.image.scaleX;
+
+      this.scale(isNumber(scaleX) ? scaleX : 1, scaleY);
+    },
+
+    /**
+     * Get the cropped area position and size data (base on the original image)
+     *
+     * @param {Boolean} isRounded (optional)
+     * @return {Object} data
+     */
+    getData: function (isRounded) {
+      var options = this.options;
+      var image = this.image;
+      var canvas = this.canvas;
+      var cropBox = this.cropBox;
+      var ratio;
+      var data;
+
+      if (this.isBuilt && this.isCropped) {
+        data = {
+          x: cropBox.left - canvas.left,
+          y: cropBox.top - canvas.top,
+          width: cropBox.width,
+          height: cropBox.height
+        };
+
+        ratio = image.width / image.naturalWidth;
+
+        $.each(data, function (i, n) {
+          n = n / ratio;
+          data[i] = isRounded ? round(n) : n;
+        });
+
+      } else {
+        data = {
+          x: 0,
+          y: 0,
+          width: 0,
+          height: 0
+        };
+      }
+
+      if (options.rotatable) {
+        data.rotate = image.rotate || 0;
+      }
+
+      if (options.scalable) {
+        data.scaleX = image.scaleX || 1;
+        data.scaleY = image.scaleY || 1;
+      }
+
+      return data;
+    },
+
+    /**
+     * Set the cropped area position and size with new data
+     *
+     * @param {Object} data
+     */
+    setData: function (data) {
+      var options = this.options;
+      var image = this.image;
+      var canvas = this.canvas;
+      var cropBoxData = {};
+      var isRotated;
+      var isScaled;
+      var ratio;
+
+      if ($.isFunction(data)) {
+        data = data.call(this.element);
+      }
+
+      if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) {
+        if (options.rotatable) {
+          if (isNumber(data.rotate) && data.rotate !== image.rotate) {
+            image.rotate = data.rotate;
+            this.isRotated = isRotated = true;
+          }
+        }
+
+        if (options.scalable) {
+          if (isNumber(data.scaleX) && data.scaleX !== image.scaleX) {
+            image.scaleX = data.scaleX;
+            isScaled = true;
+          }
+
+          if (isNumber(data.scaleY) && data.scaleY !== image.scaleY) {
+            image.scaleY = data.scaleY;
+            isScaled = true;
+          }
+        }
+
+        if (isRotated) {
+          this.renderCanvas();
+        } else if (isScaled) {
+          this.renderImage();
+        }
+
+        ratio = image.width / image.naturalWidth;
+
+        if (isNumber(data.x)) {
+          cropBoxData.left = data.x * ratio + canvas.left;
+        }
+
+        if (isNumber(data.y)) {
+          cropBoxData.top = data.y * ratio + canvas.top;
+        }
+
+        if (isNumber(data.width)) {
+          cropBoxData.width = data.width * ratio;
+        }
+
+        if (isNumber(data.height)) {
+          cropBoxData.height = data.height * ratio;
+        }
+
+        this.setCropBoxData(cropBoxData);
+      }
+    },
+
+    /**
+     * Get the container size data
+     *
+     * @return {Object} data
+     */
+    getContainerData: function () {
+      return this.isBuilt ? this.container : {};
+    },
+
+    /**
+     * Get the image position and size data
+     *
+     * @return {Object} data
+     */
+    getImageData: function () {
+      return this.isLoaded ? this.image : {};
+    },
+
+    /**
+     * Get the canvas position and size data
+     *
+     * @return {Object} data
+     */
+    getCanvasData: function () {
+      var canvas = this.canvas;
+      var data = {};
+
+      if (this.isBuilt) {
+        $.each([
+          'left',
+          'top',
+          'width',
+          'height',
+          'naturalWidth',
+          'naturalHeight'
+        ], function (i, n) {
+          data[n] = canvas[n];
+        });
+      }
+
+      return data;
+    },
+
+    /**
+     * Set the canvas position and size with new data
+     *
+     * @param {Object} data
+     */
+    setCanvasData: function (data) {
+      var canvas = this.canvas;
+      var aspectRatio = canvas.aspectRatio;
+
+      if ($.isFunction(data)) {
+        data = data.call(this.$element);
+      }
+
+      if (this.isBuilt && !this.isDisabled && $.isPlainObject(data)) {
+        if (isNumber(data.left)) {
+          canvas.left = data.left;
+        }
+
+        if (isNumber(data.top)) {
+          canvas.top = data.top;
+        }
+
+        if (isNumber(data.width)) {
+          canvas.width = data.width;
+          canvas.height = data.width / aspectRatio;
+        } else if (isNumber(data.height)) {
+          canvas.height = data.height;
+          canvas.width = data.height * aspectRatio;
+        }
+
+        this.renderCanvas(true);
+      }
+    },
+
+    /**
+     * Get the crop box position and size data
+     *
+     * @return {Object} data
+     */
+    getCropBoxData: function () {
+      var cropBox = this.cropBox;
+      var data;
+
+      if (this.isBuilt && this.isCropped) {
+        data = {
+          left: cropBox.left,
+          top: cropBox.top,
+          width: cropBox.width,
+          height: cropBox.height
+        };
+      }
+
+      return data || {};
+    },
+
+    /**
+     * Set the crop box position and size with new data
+     *
+     * @param {Object} data
+     */
+    setCropBoxData: function (data) {
+      var cropBox = this.cropBox;
+      var aspectRatio = this.options.aspectRatio;
+      var isWidthChanged;
+      var isHeightChanged;
+
+      if ($.isFunction(data)) {
+        data = data.call(this.$element);
+      }
+
+      if (this.isBuilt && this.isCropped && !this.isDisabled && $.isPlainObject(data)) {
+
+        if (isNumber(data.left)) {
+          cropBox.left = data.left;
+        }
+
+        if (isNumber(data.top)) {
+          cropBox.top = data.top;
+        }
+
+        if (isNumber(data.width)) {
+          isWidthChanged = true;
+          cropBox.width = data.width;
+        }
+
+        if (isNumber(data.height)) {
+          isHeightChanged = true;
+          cropBox.height = data.height;
+        }
+
+        if (aspectRatio) {
+          if (isWidthChanged) {
+            cropBox.height = cropBox.width / aspectRatio;
+          } else if (isHeightChanged) {
+            cropBox.width = cropBox.height * aspectRatio;
+          }
+        }
+
+        this.renderCropBox();
+      }
+    },
+
+    /**
+     * Get a canvas drawn the cropped image
+     *
+     * @param {Object} options (optional)
+     * @return {HTMLCanvasElement} canvas
+     */
+    getCroppedCanvas: function (options) {
+      var originalWidth;
+      var originalHeight;
+      var canvasWidth;
+      var canvasHeight;
+      var scaledWidth;
+      var scaledHeight;
+      var scaledRatio;
+      var aspectRatio;
+      var canvas;
+      var context;
+      var data;
+
+      if (!this.isBuilt || !this.isCropped || !SUPPORT_CANVAS) {
+        return;
+      }
+
+      if (!$.isPlainObject(options)) {
+        options = {};
+      }
+
+      data = this.getData();
+      originalWidth = data.width;
+      originalHeight = data.height;
+      aspectRatio = originalWidth / originalHeight;
+
+      if ($.isPlainObject(options)) {
+        scaledWidth = options.width;
+        scaledHeight = options.height;
+
+        if (scaledWidth) {
+          scaledHeight = scaledWidth / aspectRatio;
+          scaledRatio = scaledWidth / originalWidth;
+        } else if (scaledHeight) {
+          scaledWidth = scaledHeight * aspectRatio;
+          scaledRatio = scaledHeight / originalHeight;
+        }
+      }
+
+      // The canvas element will use `Math.floor` on a float number, so floor first
+      canvasWidth = floor(scaledWidth || originalWidth);
+      canvasHeight = floor(scaledHeight || originalHeight);
+
+      canvas = $('<canvas>')[0];
+      canvas.width = canvasWidth;
+      canvas.height = canvasHeight;
+      context = canvas.getContext('2d');
+
+      if (options.fillColor) {
+        context.fillStyle = options.fillColor;
+        context.fillRect(0, 0, canvasWidth, canvasHeight);
+      }
+
+      // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.drawImage
+      context.drawImage.apply(context, (function () {
+        var source = getSourceCanvas(this.$clone[0], this.image);
+        var sourceWidth = source.width;
+        var sourceHeight = source.height;
+        var canvas = this.canvas;
+        var params = [source];
+
+        // Source canvas
+        var srcX = data.x + canvas.naturalWidth * (abs(data.scaleX || 1) - 1) / 2;
+        var srcY = data.y + canvas.naturalHeight * (abs(data.scaleY || 1) - 1) / 2;
+        var srcWidth;
+        var srcHeight;
+
+        // Destination canvas
+        var dstX;
+        var dstY;
+        var dstWidth;
+        var dstHeight;
+
+        if (srcX <= -originalWidth || srcX > sourceWidth) {
+          srcX = srcWidth = dstX = dstWidth = 0;
+        } else if (srcX <= 0) {
+          dstX = -srcX;
+          srcX = 0;
+          srcWidth = dstWidth = min(sourceWidth, originalWidth + srcX);
+        } else if (srcX <= sourceWidth) {
+          dstX = 0;
+          srcWidth = dstWidth = min(originalWidth, sourceWidth - srcX);
+        }
+
+        if (srcWidth <= 0 || srcY <= -originalHeight || srcY > sourceHeight) {
+          srcY = srcHeight = dstY = dstHeight = 0;
+        } else if (srcY <= 0) {
+          dstY = -srcY;
+          srcY = 0;
+          srcHeight = dstHeight = min(sourceHeight, originalHeight + srcY);
+        } else if (srcY <= sourceHeight) {
+          dstY = 0;
+          srcHeight = dstHeight = min(originalHeight, sourceHeight - srcY);
+        }
+
+        // All the numerical parameters should be integer for `drawImage` (#476)
+        params.push(floor(srcX), floor(srcY), floor(srcWidth), floor(srcHeight));
+
+        // Scale destination sizes
+        if (scaledRatio) {
+          dstX *= scaledRatio;
+          dstY *= scaledRatio;
+          dstWidth *= scaledRatio;
+          dstHeight *= scaledRatio;
+        }
+
+        // Avoid "IndexSizeError" in IE and Firefox
+        if (dstWidth > 0 && dstHeight > 0) {
+          params.push(floor(dstX), floor(dstY), floor(dstWidth), floor(dstHeight));
+        }
+
+        return params;
+      }).call(this));
+
+      return canvas;
+    },
+
+    /**
+     * Change the aspect ratio of the crop box
+     *
+     * @param {Number} aspectRatio
+     */
+    setAspectRatio: function (aspectRatio) {
+      var options = this.options;
+
+      if (!this.isDisabled && !isUndefined(aspectRatio)) {
+
+        // 0 -> NaN
+        options.aspectRatio = max(0, aspectRatio) || NaN;
+
+        if (this.isBuilt) {
+          this.initCropBox();
+
+          if (this.isCropped) {
+            this.renderCropBox();
+          }
+        }
+      }
+    },
+
+    /**
+     * Change the drag mode
+     *
+     * @param {String} mode (optional)
+     */
+    setDragMode: function (mode) {
+      var options = this.options;
+      var croppable;
+      var movable;
+
+      if (this.isLoaded && !this.isDisabled) {
+        croppable = mode === ACTION_CROP;
+        movable = options.movable && mode === ACTION_MOVE;
+        mode = (croppable || movable) ? mode : ACTION_NONE;
+
+        this.$dragBox.
+          data(DATA_ACTION, mode).
+          toggleClass(CLASS_CROP, croppable).
+          toggleClass(CLASS_MOVE, movable);
+
+        if (!options.cropBoxMovable) {
+
+          // Sync drag mode to crop box when it is not movable(#300)
+          this.$face.
+            data(DATA_ACTION, mode).
+            toggleClass(CLASS_CROP, croppable).
+            toggleClass(CLASS_MOVE, movable);
+        }
+      }
+    }
+  };
+
+  Cropper.DEFAULTS = {
+
+    // Define the view mode of the cropper
+    viewMode: 0, // 0, 1, 2, 3
+
+    // Define the dragging mode of the cropper
+    dragMode: 'crop', // 'crop', 'move' or 'none'
+
+    // Define the aspect ratio of the crop box
+    aspectRatio: NaN,
+
+    // An object with the previous cropping result data
+    data: null,
+
+    // A jQuery selector for adding extra containers to preview
+    preview: '',
+
+    // Re-render the cropper when resize the window
+    responsive: true,
+
+    // Restore the cropped area after resize the window
+    restore: true,
+
+    // Check if the current image is a cross-origin image
+    checkCrossOrigin: true,
+
+    // Check the current image's Exif Orientation information
+    checkOrientation: true,
+
+    // Show the black modal
+    modal: true,
+
+    // Show the dashed lines for guiding
+    guides: true,
+
+    // Show the center indicator for guiding
+    center: true,
+
+    // Show the white modal to highlight the crop box
+    highlight: true,
+
+    // Show the grid background
+    background: true,
+
+    // Enable to crop the image automatically when initialize
+    autoCrop: true,
+
+    // Define the percentage of automatic cropping area when initializes
+    autoCropArea: 0.8,
+
+    // Enable to move the image
+    movable: true,
+
+    // Enable to rotate the image
+    rotatable: true,
+
+    // Enable to scale the image
+    scalable: true,
+
+    // Enable to zoom the image
+    zoomable: true,
+
+    // Enable to zoom the image by dragging touch
+    zoomOnTouch: true,
+
+    // Enable to zoom the image by wheeling mouse
+    zoomOnWheel: true,
+
+    // Define zoom ratio when zoom the image by wheeling mouse
+    wheelZoomRatio: 0.1,
+
+    // Enable to move the crop box
+    cropBoxMovable: true,
+
+    // Enable to resize the crop box
+    cropBoxResizable: true,
+
+    // Toggle drag mode between "crop" and "move" when click twice on the cropper
+    toggleDragModeOnDblclick: true,
+
+    // Size limitation
+    minCanvasWidth: 0,
+    minCanvasHeight: 0,
+    minCropBoxWidth: 0,
+    minCropBoxHeight: 0,
+    minContainerWidth: 200,
+    minContainerHeight: 100,
+
+    // Shortcuts of events
+    build: null,
+    built: null,
+    cropstart: null,
+    cropmove: null,
+    cropend: null,
+    crop: null,
+    zoom: null
+  };
+
+  Cropper.setDefaults = function (options) {
+    $.extend(Cropper.DEFAULTS, options);
+  };
+
+  Cropper.TEMPLATE = (
+    '<div class="cropper-container">' +
+      '<div class="cropper-wrap-box">' +
+        '<div class="cropper-canvas"></div>' +
+      '</div>' +
+      '<div class="cropper-drag-box"></div>' +
+      '<div class="cropper-crop-box">' +
+        '<span class="cropper-view-box"></span>' +
+        '<span class="cropper-dashed dashed-h"></span>' +
+        '<span class="cropper-dashed dashed-v"></span>' +
+        '<span class="cropper-center"></span>' +
+        '<span class="cropper-face"></span>' +
+        '<span class="cropper-line line-e" data-action="e"></span>' +
+        '<span class="cropper-line line-n" data-action="n"></span>' +
+        '<span class="cropper-line line-w" data-action="w"></span>' +
+        '<span class="cropper-line line-s" data-action="s"></span>' +
+        '<span class="cropper-point point-e" data-action="e"></span>' +
+        '<span class="cropper-point point-n" data-action="n"></span>' +
+        '<span class="cropper-point point-w" data-action="w"></span>' +
+        '<span class="cropper-point point-s" data-action="s"></span>' +
+        '<span class="cropper-point point-ne" data-action="ne"></span>' +
+        '<span class="cropper-point point-nw" data-action="nw"></span>' +
+        '<span class="cropper-point point-sw" data-action="sw"></span>' +
+        '<span class="cropper-point point-se" data-action="se"></span>' +
+      '</div>' +
+    '</div>'
+  );
+
+  // Save the other cropper
+  Cropper.other = $.fn.cropper;
+
+  // Register as jQuery plugin
+  $.fn.cropper = function (option) {
+    var args = toArray(arguments, 1);
+    var result;
+
+    this.each(function () {
+      var $this = $(this);
+      var data = $this.data(NAMESPACE);
+      var options;
+      var fn;
+
+      if (!data) {
+        if (/destroy/.test(option)) {
+          return;
+        }
+
+        options = $.extend({}, $this.data(), $.isPlainObject(option) && option);
+        $this.data(NAMESPACE, (data = new Cropper(this, options)));
+      }
+
+      if (typeof option === 'string' && $.isFunction(fn = data[option])) {
+        result = fn.apply(data, args);
+      }
+    });
+
+    return isUndefined(result) ? this : result;
+  };
+
+  $.fn.cropper.Constructor = Cropper;
+  $.fn.cropper.setDefaults = Cropper.setDefaults;
+
+  // No conflict
+  $.fn.cropper.noConflict = function () {
+    $.fn.cropper = Cropper.other;
+    return this;
+  };
+
+});
diff --git a/vendor/assets/stylesheets/cropper.css b/vendor/assets/stylesheets/cropper.css
new file mode 100644
index 0000000000000000000000000000000000000000..8668c7c049a84e16c0dbe3f8185041ad612733c0
--- /dev/null
+++ b/vendor/assets/stylesheets/cropper.css
@@ -0,0 +1,379 @@
+/*!
+ * Cropper v2.3.0
+ * https://github.com/fengyuanchen/cropper
+ *
+ * Copyright (c) 2014-2016 Fengyuan Chen and contributors
+ * Released under the MIT license
+ *
+ * Date: 2016-02-22T02:13:13.332Z
+ */
+.cropper-container {
+  font-size: 0;
+  line-height: 0;
+
+  position: relative;
+
+  -webkit-user-select: none;
+     -moz-user-select: none;
+      -ms-user-select: none;
+          user-select: none;
+
+  direction: ltr !important;
+  -ms-touch-action: none;
+      touch-action: none;
+  -webkit-tap-highlight-color: transparent;
+  -webkit-touch-callout: none;
+}
+
+.cropper-container img {
+  display: block;
+
+  width: 100%;
+  min-width: 0 !important;
+  max-width: none !important;
+  height: 100%;
+  min-height: 0 !important;
+  max-height: none !important;
+
+  image-orientation: 0deg !important;
+}
+
+.cropper-wrap-box,
+.cropper-canvas,
+.cropper-drag-box,
+.cropper-crop-box,
+.cropper-modal {
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+}
+
+.cropper-wrap-box {
+  overflow: hidden;
+}
+
+.cropper-drag-box {
+  opacity: 0;
+  background-color: #fff;
+
+  filter: alpha(opacity=0);
+}
+
+.cropper-modal {
+  opacity: .5;
+  background-color: #000;
+
+  filter: alpha(opacity=50);
+}
+
+.cropper-view-box {
+  display: block;
+  overflow: hidden;
+
+  width: 100%;
+  height: 100%;
+
+  outline: 1px solid #39f;
+  outline-color: rgba(51, 153, 255, .75);
+}
+
+.cropper-dashed {
+  position: absolute;
+
+  display: block;
+
+  opacity: .5;
+  border: 0 dashed #eee;
+
+  filter: alpha(opacity=50);
+}
+
+.cropper-dashed.dashed-h {
+  top: 33.33333%;
+  left: 0;
+
+  width: 100%;
+  height: 33.33333%;
+
+  border-top-width: 1px;
+  border-bottom-width: 1px;
+}
+
+.cropper-dashed.dashed-v {
+  top: 0;
+  left: 33.33333%;
+
+  width: 33.33333%;
+  height: 100%;
+
+  border-right-width: 1px;
+  border-left-width: 1px;
+}
+
+.cropper-center {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+
+  display: block;
+
+  width: 0;
+  height: 0;
+
+  opacity: .75;
+
+  filter: alpha(opacity=75);
+}
+
+.cropper-center:before,
+.cropper-center:after {
+  position: absolute;
+
+  display: block;
+
+  content: ' ';
+
+  background-color: #eee;
+}
+
+.cropper-center:before {
+  top: 0;
+  left: -3px;
+
+  width: 7px;
+  height: 1px;
+}
+
+.cropper-center:after {
+  top: -3px;
+  left: 0;
+
+  width: 1px;
+  height: 7px;
+}
+
+.cropper-face,
+.cropper-line,
+.cropper-point {
+  position: absolute;
+
+  display: block;
+
+  width: 100%;
+  height: 100%;
+
+  opacity: .1;
+
+  filter: alpha(opacity=10);
+}
+
+.cropper-face {
+  top: 0;
+  left: 0;
+
+  background-color: #fff;
+}
+
+.cropper-line {
+  background-color: #39f;
+}
+
+.cropper-line.line-e {
+  top: 0;
+  right: -3px;
+
+  width: 5px;
+
+  cursor: e-resize;
+}
+
+.cropper-line.line-n {
+  top: -3px;
+  left: 0;
+
+  height: 5px;
+
+  cursor: n-resize;
+}
+
+.cropper-line.line-w {
+  top: 0;
+  left: -3px;
+
+  width: 5px;
+
+  cursor: w-resize;
+}
+
+.cropper-line.line-s {
+  bottom: -3px;
+  left: 0;
+
+  height: 5px;
+
+  cursor: s-resize;
+}
+
+.cropper-point {
+  width: 5px;
+  height: 5px;
+
+  opacity: .75;
+  background-color: #39f;
+
+  filter: alpha(opacity=75);
+}
+
+.cropper-point.point-e {
+  top: 50%;
+  right: -3px;
+
+  margin-top: -3px;
+
+  cursor: e-resize;
+}
+
+.cropper-point.point-n {
+  top: -3px;
+  left: 50%;
+
+  margin-left: -3px;
+
+  cursor: n-resize;
+}
+
+.cropper-point.point-w {
+  top: 50%;
+  left: -3px;
+
+  margin-top: -3px;
+
+  cursor: w-resize;
+}
+
+.cropper-point.point-s {
+  bottom: -3px;
+  left: 50%;
+
+  margin-left: -3px;
+
+  cursor: s-resize;
+}
+
+.cropper-point.point-ne {
+  top: -3px;
+  right: -3px;
+
+  cursor: ne-resize;
+}
+
+.cropper-point.point-nw {
+  top: -3px;
+  left: -3px;
+
+  cursor: nw-resize;
+}
+
+.cropper-point.point-sw {
+  bottom: -3px;
+  left: -3px;
+
+  cursor: sw-resize;
+}
+
+.cropper-point.point-se {
+  right: -3px;
+  bottom: -3px;
+
+  width: 20px;
+  height: 20px;
+
+  cursor: se-resize;
+
+  opacity: 1;
+
+  filter: alpha(opacity=100);
+}
+
+.cropper-point.point-se:before {
+  position: absolute;
+  right: -50%;
+  bottom: -50%;
+
+  display: block;
+
+  width: 200%;
+  height: 200%;
+
+  content: ' ';
+
+  opacity: 0;
+  background-color: #39f;
+
+  filter: alpha(opacity=0);
+}
+
+@media (min-width: 768px) {
+  .cropper-point.point-se {
+    width: 15px;
+    height: 15px;
+  }
+}
+
+@media (min-width: 992px) {
+  .cropper-point.point-se {
+    width: 10px;
+    height: 10px;
+  }
+}
+
+@media (min-width: 1200px) {
+  .cropper-point.point-se {
+    width: 5px;
+    height: 5px;
+
+    opacity: .75;
+
+    filter: alpha(opacity=75);
+  }
+}
+
+.cropper-invisible {
+  opacity: 0;
+
+  filter: alpha(opacity=0);
+}
+
+.cropper-bg {
+  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
+}
+
+.cropper-hide {
+  position: absolute;
+
+  display: block;
+
+  width: 0;
+  height: 0;
+}
+
+.cropper-hidden {
+  display: none !important;
+}
+
+.cropper-move {
+  cursor: move;
+}
+
+.cropper-crop {
+  cursor: crosshair;
+}
+
+.cropper-disabled .cropper-drag-box,
+.cropper-disabled .cropper-face,
+.cropper-disabled .cropper-line,
+.cropper-disabled .cropper-point {
+  cursor: not-allowed;
+}