From 5f9857113ec5a00d31cff8a743a7b94a2ae5f586 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 6 Jul 2016 14:48:25 +0100 Subject: Fixed enter submitting form in dropdown Closes #19549 --- app/assets/javascripts/gl_dropdown.js.coffee | 55 ++++++++++++++-------------- app/assets/javascripts/project.js.coffee | 7 +++- 2 files changed, 34 insertions(+), 28 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 1b0d0db8954..bfed7e438c4 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -24,33 +24,34 @@ class GitLabDropdownFilter # Key events timeout = "" - @input.on "keyup", (e) => - keyCode = e.which - - return if ARROW_KEY_CODES.indexOf(keyCode) >= 0 - - 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 keyCode is 13 - return false - - # Only filter asynchronously only if option remote is set - if @options.remote - clearTimeout timeout - timeout = setTimeout => - blur_field = @shouldBlur keyCode - - if blur_field and @filterInputBlur - @input.blur() - - @options.query @input.val(), (data) => - @options.callback(data) - , 250 - else - @filter @input.val() + @input + .on 'keydown', (e) -> + keyCode = e.which + e.preventDefault() if keyCode is 13 + .on 'keyup', (e) => + keyCode = e.which + + return if ARROW_KEY_CODES.indexOf(keyCode) >= 0 + + 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 + + # Only filter asynchronously only if option remote is set + if @options.remote + clearTimeout timeout + timeout = setTimeout => + blur_field = @shouldBlur keyCode + + if blur_field and @filterInputBlur + @input.blur() + + @options.query @input.val(), (data) => + @options.callback(data) + , 250 + else + @filter @input.val() shouldBlur: (keyCode) -> return BLUR_KEYCODES.indexOf(keyCode) >= 0 diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 3288c801388..80ea179948d 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -87,5 +87,10 @@ class @Project toggleLabel: (obj, $el) -> $el.text().trim() clicked: (e) -> - $dropdown.closest('form').submit() + if $('input[name="ref"]').length + $form = $dropdown.closest('form') + action = $form.attr('action') + divider = if action.indexOf('?') < 0 then '?' else '&' + + Turbolinks.visit "#{action}#{divider}#{$form.serialize()}" ) -- cgit v1.2.3 From f91cfa8ea48ab975c9f33085c6b13ae1b27c6025 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 14 Jul 2016 09:07:37 +0100 Subject: Fixed navigational keys not working when filtering --- app/assets/javascripts/gl_dropdown.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index bfed7e438c4..3688d049a83 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -554,7 +554,7 @@ class GitLabDropdown ARROW_KEY_CODES = [38, 40] $input = @dropdown.find(".dropdown-input-field") - selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator)' + selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible' if @dropdown.find(".dropdown-toggle-page").length selector = ".dropdown-page-one #{selector}" -- cgit v1.2.3 From 1dc706dbd57b3d63e02bbe04c9a1a152ef9af34f Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 18 Jul 2016 16:43:13 -0500 Subject: Improve conditional --- app/assets/javascripts/gl_dropdown.js.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index 3688d049a83..bd814dd15a2 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -31,7 +31,7 @@ class GitLabDropdownFilter .on 'keyup', (e) => keyCode = e.which - return if ARROW_KEY_CODES.indexOf(keyCode) >= 0 + return unless ARROW_KEY_CODES.indexOf(keyCode) is -1 if @input.val() isnt '' and !$inputContainer.hasClass HAS_VALUE_CLASS $inputContainer.addClass HAS_VALUE_CLASS -- cgit v1.2.3 From 5d7b2bd44e08414addde7eb700e5ccf21f59f94c Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 19 Jul 2016 08:29:57 +0100 Subject: Prevent default on link click in ref switcher --- app/assets/javascripts/project.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 80ea179948d..2a8e8f2552b 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -86,7 +86,8 @@ class @Project $el.attr('data-ref') toggleLabel: (obj, $el) -> $el.text().trim() - clicked: (e) -> + clicked: (selected, $el, e) -> + e.preventDefault() if $('input[name="ref"]').length $form = $dropdown.closest('form') action = $form.attr('action') -- cgit v1.2.3 From 896b9c1dc80af349bab9681bb0c668de65151572 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 19 Jul 2016 19:52:40 +0100 Subject: Fixed issue with filtering & pressing enter key --- app/assets/javascripts/gl_dropdown.js.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee index bd814dd15a2..ab41caa28f8 100644 --- a/app/assets/javascripts/gl_dropdown.js.coffee +++ b/app/assets/javascripts/gl_dropdown.js.coffee @@ -311,6 +311,7 @@ class GitLabDropdown return true opened: => + currentIndex = -1 @addArrowKeyEvent() if @options.setIndeterminateIds @@ -568,9 +569,6 @@ class GitLabDropdown PREV_INDEX = currentIndex $listItems = $(selector, @dropdown) - # if @options.filterable - # $input.blur() - if currentKeyCode is 40 # Move down currentIndex += 1 if currentIndex < ($listItems.length - 1) @@ -583,7 +581,7 @@ class GitLabDropdown return false if currentKeyCode is 13 and currentIndex isnt -1 - @selectRowAtIndex e, currentIndex + @selectRowAtIndex e, $('.is-focused', @dropdown).closest('li').index() - 1 removeArrayKeyEvent: -> $('body').off 'keydown' -- cgit v1.2.3 From 26055b16b58afd73e31f7aaacb9aaa79ba3794c2 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 5 Jul 2016 13:52:20 +0100 Subject: Highlight empty lines Closes #19484 --- app/assets/stylesheets/framework/highlight.scss | 10 ++++++++-- app/views/projects/blob/_editor.html.haml | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 7cf4d4fba42..51ae9df9685 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -6,11 +6,11 @@ table-layout: fixed; pre { - padding: 10px; + padding: 10px 0; border: none; border-radius: 0; font-family: $monospace_font; - font-size: $code_font_size !important; + font-size: 0; line-height: $code_line_height !important; margin: 0; overflow: auto; @@ -21,12 +21,18 @@ code { font-family: $monospace_font; + font-size: 0; white-space: pre; word-wrap: normal; padding: 0; .line { display: inline-block; + width: 100%; + min-height: 19px; + padding-left: 10px; + padding-right: 10px; + font-size: $code_font_size; } } } diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index ff379bafb26..0237e152b54 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -24,7 +24,7 @@ .encoding-selector = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2' - .file-content.code + .file-editor.code %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]} - if local_assigns[:path] .js-edit-mode-pane#preview.hide -- cgit v1.2.3 From 5cf89e70b70de008d5b91e89ce015522616e96cb Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 14 Jul 2016 09:12:10 +0100 Subject: Uses white-space instead of setting font size to 0 --- app/assets/stylesheets/framework/highlight.scss | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 51ae9df9685..11b2a4cbf89 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -10,7 +10,7 @@ border: none; border-radius: 0; font-family: $monospace_font; - font-size: 0; + font-size: $code_font_size; line-height: $code_line_height !important; margin: 0; overflow: auto; @@ -21,18 +21,16 @@ code { font-family: $monospace_font; - font-size: 0; - white-space: pre; + white-space: normal; word-wrap: normal; padding: 0; .line { - display: inline-block; + display: block; width: 100%; min-height: 19px; padding-left: 10px; padding-right: 10px; - font-size: $code_font_size; } } } -- cgit v1.2.3 From cf33acb32b12cf482e0279043e8cd02131c456e6 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Tue, 19 Jul 2016 19:19:22 +0100 Subject: Fixed wrapping of lines on smaller viewports --- app/assets/stylesheets/framework/highlight.scss | 3 +++ 1 file changed, 3 insertions(+) (limited to 'app') diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index 11b2a4cbf89..07c8874bf03 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -20,6 +20,8 @@ border-left: 1px solid; code { + display: inline-block; + min-width: 100%; font-family: $monospace_font; white-space: normal; word-wrap: normal; @@ -31,6 +33,7 @@ min-height: 19px; padding-left: 10px; padding-right: 10px; + white-space: pre; } } } -- cgit v1.2.3 From 283bd88a77786b44d2f491f73a1b22be802fad68 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 21 Jul 2016 12:50:30 +0100 Subject: Changed project description width Closes #20020 --- app/assets/stylesheets/pages/projects.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index cc3aef5199e..53cdc3a1400 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -99,7 +99,7 @@ margin-left: auto; margin-right: auto; margin-bottom: 15px; - max-width: 480px; + max-width: 700px; > p { margin-bottom: 0; -- cgit v1.2.3 From 0d9752446d8e2b3b4fdb37eb8ec75c5e5f996f1c Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 20 Jul 2016 18:41:26 +0200 Subject: Add LFS controllers --- .../projects/git_http_client_controller.rb | 110 +++++++++++++++++++++ app/controllers/projects/git_http_controller.rb | 107 +------------------- app/controllers/projects/lfs_api_controller.rb | 94 ++++++++++++++++++ app/controllers/projects/lfs_storage_controller.rb | 106 ++++++++++++++++++++ app/helpers/lfs_helper.rb | 66 +++++++++++++ 5 files changed, 379 insertions(+), 104 deletions(-) create mode 100644 app/controllers/projects/git_http_client_controller.rb create mode 100644 app/controllers/projects/lfs_api_controller.rb create mode 100644 app/controllers/projects/lfs_storage_controller.rb create mode 100644 app/helpers/lfs_helper.rb (limited to 'app') diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb new file mode 100644 index 00000000000..7c21bd181dc --- /dev/null +++ b/app/controllers/projects/git_http_client_controller.rb @@ -0,0 +1,110 @@ +# This file should be identical in GitLab Community Edition and Enterprise Edition + +class Projects::GitHttpClientController < Projects::ApplicationController + include ActionController::HttpAuthentication::Basic + include KerberosSpnegoHelper + + attr_reader :user + + # Git clients will not know what authenticity token to send along + skip_before_action :verify_authenticity_token + skip_before_action :repository + before_action :authenticate_user + before_action :ensure_project_found! + + private + + def authenticate_user + if project && project.public? && download_request? + return # Allow access + end + + if allow_basic_auth? && basic_auth_provided? + login, password = user_name_and_password(request) + auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip) + + if auth_result.type == :ci && download_request? + @ci = true + elsif auth_result.type == :oauth && !download_request? + # Not allowed + else + @user = auth_result.user + end + + if ci? || user + return # Allow access + end + elsif allow_kerberos_spnego_auth? && spnego_provided? + @user = find_kerberos_user + + if user + send_final_spnego_response + return # Allow access + end + end + + send_challenges + render plain: "HTTP Basic: Access denied\n", status: 401 + end + + def basic_auth_provided? + has_basic_credentials?(request) + end + + def send_challenges + challenges = [] + challenges << 'Basic realm="GitLab"' if allow_basic_auth? + challenges << spnego_challenge if allow_kerberos_spnego_auth? + headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? + end + + def ensure_project_found! + render_not_found if project.blank? + end + + def project + return @project if defined?(@project) + + project_id, _ = project_id_with_suffix + if project_id.blank? + @project = nil + else + @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") + end + end + + # This method returns two values so that we can parse + # params[:project_id] (untrusted input!) in exactly one place. + def project_id_with_suffix + id = params[:project_id] || '' + + %w[.wiki.git .git].each do |suffix| + if id.end_with?(suffix) + # Be careful to only remove the suffix from the end of 'id'. + # Accidentally removing it from the middle is how security + # vulnerabilities happen! + return [id.slice(0, id.length - suffix.length), suffix] + end + end + + # Something is wrong with params[:project_id]; do not pass it on. + [nil, nil] + end + + def repository + _, suffix = project_id_with_suffix + if suffix == '.wiki.git' + project.wiki.repository + else + project.repository + end + end + + def render_not_found + render plain: 'Not Found', status: :not_found + end + + def ci? + @ci.present? + end +end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 40a8b7940d9..be73a4c0d2c 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,17 +1,6 @@ # This file should be identical in GitLab Community Edition and Enterprise Edition -class Projects::GitHttpController < Projects::ApplicationController - include ActionController::HttpAuthentication::Basic - include KerberosSpnegoHelper - - attr_reader :user - - # Git clients will not know what authenticity token to send along - skip_before_action :verify_authenticity_token - skip_before_action :repository - before_action :authenticate_user - before_action :ensure_project_found! - +class Projects::GitHttpController < Projects::GitHttpClientController # GET /foo/bar.git/info/refs?service=git-upload-pack (git pull) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push) def info_refs @@ -46,81 +35,8 @@ class Projects::GitHttpController < Projects::ApplicationController private - def authenticate_user - if project && project.public? && upload_pack? - return # Allow access - end - - if allow_basic_auth? && basic_auth_provided? - login, password = user_name_and_password(request) - auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip) - - if auth_result.type == :ci && upload_pack? - @ci = true - elsif auth_result.type == :oauth && !upload_pack? - # Not allowed - else - @user = auth_result.user - end - - if ci? || user - return # Allow access - end - elsif allow_kerberos_spnego_auth? && spnego_provided? - @user = find_kerberos_user - - if user - send_final_spnego_response - return # Allow access - end - end - - send_challenges - render plain: "HTTP Basic: Access denied\n", status: 401 - end - - def basic_auth_provided? - has_basic_credentials?(request) - end - - def send_challenges - challenges = [] - challenges << 'Basic realm="GitLab"' if allow_basic_auth? - challenges << spnego_challenge if allow_kerberos_spnego_auth? - headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? - end - - def ensure_project_found! - render_not_found if project.blank? - end - - def project - return @project if defined?(@project) - - project_id, _ = project_id_with_suffix - if project_id.blank? - @project = nil - else - @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") - end - end - - # This method returns two values so that we can parse - # params[:project_id] (untrusted input!) in exactly one place. - def project_id_with_suffix - id = params[:project_id] || '' - - %w[.wiki.git .git].each do |suffix| - if id.end_with?(suffix) - # Be careful to only remove the suffix from the end of 'id'. - # Accidentally removing it from the middle is how security - # vulnerabilities happen! - return [id.slice(0, id.length - suffix.length), suffix] - end - end - - # Something is wrong with params[:project_id]; do not pass it on. - [nil, nil] + def download_request? + upload_pack? end def upload_pack? @@ -143,27 +59,10 @@ class Projects::GitHttpController < Projects::ApplicationController render json: Gitlab::Workhorse.git_http_ok(repository, user) end - def repository - _, suffix = project_id_with_suffix - if suffix == '.wiki.git' - project.wiki.repository - else - project.repository - end - end - - def render_not_found - render plain: 'Not Found', status: :not_found - end - def render_not_allowed render plain: download_access.message, status: :forbidden end - def ci? - @ci.present? - end - def upload_pack_allowed? return false unless Gitlab.config.gitlab_shell.upload_pack diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb new file mode 100644 index 00000000000..694ce5a0aa1 --- /dev/null +++ b/app/controllers/projects/lfs_api_controller.rb @@ -0,0 +1,94 @@ +class Projects::LfsApiController < Projects::GitHttpClientController + include LfsHelper + + before_action :lfs_enabled! + before_action :lfs_check_access!, except: [:deprecated] + + def batch + unless objects.present? + render_lfs_not_found + return + end + + if download_request? + render json: { objects: download_objects! } + elsif upload_request? + render json: { objects: upload_objects! } + else + raise "Never reached" + end + end + + def deprecated + render( + json: { + message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + status: 501 + ) + end + + private + + def objects + (params[:objects] || []).to_a + end + + def existing_oids + @existing_oids ||= begin + storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) + end + end + + def download_objects! + objects.each do |object| + if existing_oids.include?(object[:oid]) + object[:actions] = download_actions(object) + else + object[:error] = { + code: 404, + message: "Object does not exist on the server or you don't have permissions to access it", + } + end + end + objects + end + + def upload_objects! + objects.each do |object| + object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid]) + end + objects + end + + def download_actions(object) + { + download: { + href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}", + header: { + Authorization: request.headers['Authorization'] + }.compact + } + } + end + + def upload_actions(object) + { + upload: { + href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}", + header: { + Authorization: request.headers['Authorization'] + }.compact + } + } + end + + def download_request? + params[:operation] == 'download' + end + + def upload_request? + params[:operation] == 'upload' + end +end diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb new file mode 100644 index 00000000000..3aa08bcd4ae --- /dev/null +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -0,0 +1,106 @@ +class Projects::LfsStorageController < Projects::GitHttpClientController + include LfsHelper + + before_action :lfs_enabled! + before_action :lfs_check_access! + + def download + lfs_object = LfsObject.find_by_oid(oid) + unless lfs_object && lfs_object.file.exists? + render_lfs_not_found + return + end + + send_file lfs_object.file.path, content_type: "application/octet-stream" + end + + def upload_authorize + render( + json: { + StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload", + LfsOid: oid, + LfsSize: size, + }, + content_type: 'application/json; charset=utf-8' + ) + end + + def upload_finalize + unless tmp_filename + render_lfs_forbidden + return + end + + if store_file(oid, size, tmp_filename) + head 200 + else + render plain: 'Unprocessable entity', status: 422 + end + end + + private + + def download_request? + action_name == 'download' + end + + def upload_request? + %w[upload_authorize upload_finalize].include? action_name + end + + def oid + params[:oid].to_s + end + + def size + params[:size].to_i + end + + def tmp_filename + name = request.headers['X-Gitlab-Lfs-Tmp'] + if name.present? + name.gsub!(/^.*(\\|\/)/, '') + name = name.match(/[0-9a-f]{73}/) + name[0] if name + else + nil + end + end + + def store_file(oid, size, tmp_file) + tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file) + + object = LfsObject.find_or_create_by(oid: oid, size: size) + if object.file.exists? + success = true + else + success = move_tmp_file_to_storage(object, tmp_file_path) + end + + if success + success = link_to_project(object) + end + + success + ensure + # Ensure that the tmp file is removed + FileUtils.rm_f(tmp_file_path) + end + + def move_tmp_file_to_storage(object, path) + File.open(path) do |f| + object.file = f + end + + object.file.store! + object.save + end + + def link_to_project(object) + if object && !object.projects.exists?(storage_project.id) + object.projects << storage_project + object.save + end + end +end + diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb new file mode 100644 index 00000000000..6146a2ad849 --- /dev/null +++ b/app/helpers/lfs_helper.rb @@ -0,0 +1,66 @@ +module LfsHelper + def lfs_enabled! + return if Gitlab.config.lfs.enabled + + render( + json: { + message: 'Git LFS is not enabled on this GitLab server, contact your admin.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + status: 501 + ) + end + + def lfs_check_access! + return if download_request? && lfs_download_access? + return if upload_request? && lfs_upload_access? + + if project.public? || (user && user.can?(:read_project, project)) + render_lfs_forbidden + else + render_lfs_not_found + end + end + + def lfs_download_access? + project.public? || ci? || (user && user.can?(:download_code, project)) + end + + def lfs_upload_access? + user && user.can?(:push_code, project) + end + + def render_lfs_forbidden + render( + json: { + message: 'Access forbidden. Check your access level.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + content_type: "application/vnd.git-lfs+json", + status: 403 + ) + end + + def render_lfs_not_found + render( + json: { + message: 'Not found.', + documentation_url: "#{Gitlab.config.gitlab.url}/help", + }, + content_type: "application/vnd.git-lfs+json", + status: 404 + ) + end + + def storage_project + @storage_project ||= begin + result = project + + while result.forked? do + result = result.forked_from_project + end + + result + end + end +end -- cgit v1.2.3 From d199b3cdd7a43d46d88a6386b95b48c0b40b8315 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Jul 2016 15:56:10 +0200 Subject: Better cache when modifying in-place --- app/controllers/projects/lfs_api_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index 694ce5a0aa1..1fa9cd8f47f 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -32,7 +32,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController private def objects - (params[:objects] || []).to_a + @objects ||= (params[:objects] || []).to_a end def existing_oids -- cgit v1.2.3 From 23425401d1b574dd87babfffda4d59b9f91d1538 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Fri, 22 Jul 2016 16:40:22 +0200 Subject: Rubocop --- app/controllers/projects/lfs_storage_controller.rb | 13 ++++++------- app/helpers/lfs_helper.rb | 3 ++- 2 files changed, 8 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 3aa08bcd4ae..d0895e1324c 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -16,12 +16,12 @@ class Projects::LfsStorageController < Projects::GitHttpClientController def upload_authorize render( - json: { - StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload", - LfsOid: oid, - LfsSize: size, - }, - content_type: 'application/json; charset=utf-8' + json: { + StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload", + LfsOid: oid, + LfsSize: size, + }, + content_type: 'application/json; charset=utf-8' ) end @@ -103,4 +103,3 @@ class Projects::LfsStorageController < Projects::GitHttpClientController end end end - diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index 6146a2ad849..ae230ee1878 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -56,7 +56,8 @@ module LfsHelper @storage_project ||= begin result = project - while result.forked? do + loop do + break unless result.forked? result = result.forked_from_project end -- cgit v1.2.3 From 1799b4a1ac4d5f27e417982c06e8ed3d27ff39d1 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 25 Jul 2016 15:15:44 -0500 Subject: Align visibility icons on group page --- app/views/shared/projects/_project.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index b8b66d08db8..92803838d02 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -24,7 +24,7 @@ = icon('star') = project.star_count %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} - = visibility_level_icon(project.visibility_level, fw: false) + = visibility_level_icon(project.visibility_level, fw: true) .title = link_to project_path(project), class: dom_class(project) do -- cgit v1.2.3 From db19e72027f21e573eb7889791e37ffa14a9a925 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 22 Jul 2016 17:28:39 -0700 Subject: Add route, controller action, and views for MR pipelines --- .../javascripts/merge_request_tabs.js.coffee | 266 +++++++++++++++++++++ .../javascripts/merge_request_widget.js.coffee | 143 +++++++++++ .../projects/merge_requests_controller.rb | 19 +- app/views/projects/commit/_pipelines_list.haml | 54 +++++ app/views/projects/merge_requests/_show.html.haml | 6 + .../projects/merge_requests/show/_builds.html.haml | 1 - .../merge_requests/show/_pipelines.html.haml | 1 + .../projects/merge_requests/widget/_show.html.haml | 3 +- 8 files changed, 487 insertions(+), 6 deletions(-) create mode 100644 app/assets/javascripts/merge_request_tabs.js.coffee create mode 100644 app/assets/javascripts/merge_request_widget.js.coffee create mode 100644 app/views/projects/commit/_pipelines_list.haml create mode 100644 app/views/projects/merge_requests/show/_pipelines.html.haml (limited to 'app') diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee new file mode 100644 index 00000000000..ccdfcf895a3 --- /dev/null +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -0,0 +1,266 @@ +# MergeRequestTabs +# +# Handles persisting and restoring the current tab selection and lazily-loading +# content on the MergeRequests#show page. +# +#= require jquery.cookie +# +# ### Example Markup +# +# +# +#
+#
+# Notes Content +#
+#
+# Commits Content +#
+#
+# Diffs Content +#
+#
+# +#
+#
+# Loading Animation +#
+#
+# +class @MergeRequestTabs + diffsLoaded: false + buildsLoaded: false + commitsLoaded: false + + constructor: (@opts = {}) -> + # Store the `location` object, allowing for easier stubbing in tests + @_location = location + + @bindEvents() + @activateTab(@opts.action) + + bindEvents: -> + $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown + $(document).on 'click', '.js-show-tab', @showTab + + showTab: (event) => + event.preventDefault() + + @activateTab $(event.target).data('action') + + tabShown: (event) => + $target = $(event.target) + action = $target.data('action') + + if action == 'commits' + @loadCommits($target.attr('href')) + @expandView() + else if action == 'diffs' + @loadDiff($target.attr('href')) + if bp? and bp.getBreakpointSize() isnt 'lg' + @shrinkView() + + navBarHeight = $('.navbar-gitlab').outerHeight() + $.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight) + else if action == 'builds' + @loadBuilds($target.attr('href')) + @expandView() + else if action == 'pipelines' + @loadPipelines($target.attr('href')) + @expandView() + else + @expandView() + + @setCurrentAction(action) + + scrollToElement: (container) -> + if window.location.hash + navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + + $el = $("#{container} #{window.location.hash}:not(.match)") + $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length + + # Activate a tab based on the current action + activateTab: (action) -> + action = 'notes' if action == 'show' + $(".merge-request-tabs a[data-action='#{action}']").tab('show') + + # Replaces the current Merge Request-specific action in the URL with a new one + # + # If the action is "notes", the URL is reset to the standard + # `MergeRequests#show` route. + # + # Examples: + # + # location.pathname # => "/namespace/project/merge_requests/1" + # setCurrentAction('diffs') + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('notes') + # location.pathname # => "/namespace/project/merge_requests/1" + # + # location.pathname # => "/namespace/project/merge_requests/1/diffs" + # setCurrentAction('commits') + # location.pathname # => "/namespace/project/merge_requests/1/commits" + # + # Returns the new URL String + setCurrentAction: (action) => + # Normalize action, just to be safe + action = 'notes' if action == 'show' + + # Remove a trailing '/commits' or '/diffs' + new_state = @_location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '') + + # Append the new action if we're on a tab other than 'notes' + unless action == 'notes' + new_state += "/#{action}" + + # Ensure parameters and hash come along for the ride + new_state += @_location.search + @_location.hash + + # Replace the current history state with the new one without breaking + # Turbolinks' history. + # + # See https://github.com/rails/turbolinks/issues/363 + history.replaceState {turbolinks: true, url: new_state}, document.title, new_state + + new_state + + loadCommits: (source) -> + return if @commitsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.querySelector("div#commits").innerHTML = data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#commits')) + @commitsLoaded = true + @scrollToElement("#commits") + + loadDiff: (source) -> + return if @diffsLoaded + @_get + url: "#{source}.json" + @_location.search + success: (data) => + $('#diffs').html data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) + $('#diffs .js-syntax-highlight').syntaxHighlight() + $('#diffs .diff-file').singleFileDiff() + @expandViewContainer() if @diffViewType() is 'parallel' + @diffsLoaded = true + @scrollToElement("#diffs") + @highlighSelectedLine() + @filesCommentButton = $('.files .diff-file').filesCommentButton() + + $(document) + .off 'click', '.diff-line-num a' + .on 'click', '.diff-line-num a', (e) => + e.preventDefault() + window.location.hash = $(e.currentTarget).attr 'href' + @highlighSelectedLine() + @scrollToElement("#diffs") + + highlighSelectedLine: -> + $('.hll').removeClass 'hll' + locationHash = window.location.hash + + if locationHash isnt '' + hashClassString = ".#{locationHash.replace('#', '')}" + $diffLine = $("#{locationHash}:not(.match)", $('#diffs')) + + if not $diffLine.is 'tr' + $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}") + else + $diffLine = $diffLine.find('td') + + if $diffLine.length + $diffLine.addClass 'hll' + diffLineTop = $diffLine.offset().top + navBarHeight = $('.navbar-gitlab').outerHeight() + + loadBuilds: (source) -> + return if @buildsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.querySelector("div#builds").innerHTML = data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#builds')) + @buildsLoaded = true + @scrollToElement("#builds") + + loadPipelines: (source) -> + return if @pipelinesLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.querySelector("div#pipelines").innerHTML = data.html + gl.utils.localTimeAgo($('.js-timeago', 'div#pipelines')) + @pipelinesLoaded = true + @scrollToElement("#pipelines") + + # Show or hide the loading spinner + # + # status - Boolean, true to show, false to hide + toggleLoading: (status) -> + $('.mr-loading-status .loading').toggle(status) + + _get: (options) -> + defaults = { + beforeSend: => @toggleLoading(true) + complete: => @toggleLoading(false) + dataType: 'json' + type: 'GET' + } + + options = $.extend({}, defaults, options) + + $.ajax(options) + + # Returns diff view type + diffViewType: -> + $('.inline-parallel-buttons a.active').data('view-type') + + expandViewContainer: -> + $('.container-fluid').removeClass('container-limited') + + shrinkView: -> + $gutterIcon = $('.js-sidebar-toggle i:visible') + + # Wait until listeners are set + setTimeout( -> + # Only when sidebar is expanded + if $gutterIcon.is('.fa-angle-double-right') + $gutterIcon.closest('a').trigger('click', [true]) + , 0) + + # Expand the issuable sidebar unless the user explicitly collapsed it + expandView: -> + return if $.cookie('collapsed_gutter') == 'true' + + $gutterIcon = $('.js-sidebar-toggle i:visible') + + # Wait until listeners are set + setTimeout( -> + # Only when sidebar is collapsed + if $gutterIcon.is('.fa-angle-double-left') + $gutterIcon.closest('a').trigger('click', [true]) + , 0) diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee new file mode 100644 index 00000000000..63a12582ddc --- /dev/null +++ b/app/assets/javascripts/merge_request_widget.js.coffee @@ -0,0 +1,143 @@ +class @MergeRequestWidget + # Initialize MergeRequestWidget behavior + # + # check_enable - Boolean, whether to check automerge status + # merge_check_url - String, URL to use to check automerge status + # ci_status_url - String, URL to use to check CI status + # + + constructor: (@opts) -> + $('#modal_merge_info').modal(show: false) + @firstCICheck = true + @readyForCICheck = false + @cancel = false + clearInterval @fetchBuildStatusInterval + + @clearEventListeners() + @addEventListeners() + @getCIStatus(false) + @pollCIStatus() + notifyPermissions() + + clearEventListeners: -> + $(document).off 'page:change.merge_request' + + cancelPolling: -> + @cancel = true + + addEventListeners: -> + allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'] + $(document).on 'page:change.merge_request', => + page = $('body').data('page').split(':').last() + if allowedPages.indexOf(page) < 0 + clearInterval @fetchBuildStatusInterval + @cancelPolling() + @clearEventListeners() + + mergeInProgress: (deleteSourceBranch = false)-> + $.ajax + type: 'GET' + url: $('.merge-request').data('url') + success: (data) => + if data.state == "merged" + urlSuffix = if deleteSourceBranch then '?delete_source=true' else '' + + window.location.href = window.location.pathname + urlSuffix + else if data.merge_error + $('.mr-widget-body').html("

" + data.merge_error + "

") + else + callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch) + setTimeout(callback, 2000) + dataType: 'json' + + getMergeStatus: -> + $.get @opts.merge_check_url, (data) -> + $('.mr-state-widget').replaceWith(data) + + ciLabelForStatus: (status) -> + switch status + when 'success' + 'passed' + when 'success_with_warnings' + 'passed with warnings' + else + status + + pollCIStatus: -> + @fetchBuildStatusInterval = setInterval ( => + return if not @readyForCICheck + + @getCIStatus(true) + + @readyForCICheck = false + ), 10000 + + getCIStatus: (showNotification) -> + _this = @ + $('.ci-widget-fetching').show() + + $.getJSON @opts.ci_status_url, (data) => + return if @cancel + @readyForCICheck = true + + if data.status is '' + return + + if @firstCICheck || data.status isnt @opts.ci_status and data.status? + @opts.ci_status = data.status + @showCIStatus data.status + if data.coverage + @showCICoverage data.coverage + + # The first check should only update the UI, a notification + # should only be displayed on status changes + if showNotification and not @firstCICheck + status = @ciLabelForStatus(data.status) + + if status is "preparing" + title = @opts.ci_title.preparing + status = status.charAt(0).toUpperCase() + status.slice(1); + message = @opts.ci_message.preparing.replace('{{status}}', status) + else + title = @opts.ci_title.normal + message = @opts.ci_message.normal.replace('{{status}}', status) + + title = title.replace('{{status}}', status) + message = message.replace('{{sha}}', data.sha) + message = message.replace('{{title}}', data.title) + + notify( + title, + message, + @opts.gitlab_icon, + -> + @close() + Turbolinks.visit _this.opts.builds_path + ) + @firstCICheck = false + + showCIStatus: (state) -> + return if not state? + $('.ci_widget').hide() + allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"] + if state in allowed_states + $('.ci_widget.ci-' + state).show() + switch state + when "failed", "canceled", "not_found" + @setMergeButtonClass('btn-danger') + when "running" + @setMergeButtonClass('btn-warning') + when "success", "success_with_warnings" + @setMergeButtonClass('btn-create') + else + $('.ci_widget.ci-error').show() + @setMergeButtonClass('btn-danger') + + showCICoverage: (coverage) -> + text = 'Coverage ' + coverage + '%' + $('.ci_widget:visible .ci-coverage').text(text) + + setMergeButtonClass: (css_class) -> + $('.js-merge-button,.accept-action .dropdown-toggle') + .removeClass('btn-danger btn-warning btn-create') + .addClass(css_class) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 594a61464b9..5c6396fba9f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,15 +7,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, + :edit, :update, :show, :diffs, :commits, :builds, :pipelines, :merge, :merge_check, :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip ] - before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] - before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] + before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] + before_action :define_show_vars, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_commit_vars, only: [:diffs] before_action :define_diff_comment_vars, only: [:diffs] - before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] + before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :pipelines] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -136,6 +136,17 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def pipelines + respond_to do |format| + format.html do + define_discussion_vars + + render 'show' + end + format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_pipelines') } } + end + end + def new build_merge_request @noteable = @merge_request diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml new file mode 100644 index 00000000000..2c2feced657 --- /dev/null +++ b/app/views/projects/commit/_pipelines_list.haml @@ -0,0 +1,54 @@ +- status = pipeline.status + +%ul.content-list.pipelines + + .table-holder + %table.table.builds + %tbody + %th Status + %th Commit + %th.stage + %th + %th + %tr.commit + %td.commit-link + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do + = ci_status_with_icon(status) + %td + .branch-commit + = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do + %span ##{pipeline.id} + - if pipeline.ref + .icon-container + = pipeline.tag? ? icon('tag') : icon('code-fork') + = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name" + .icon-container + = custom_icon("icon_commit") + = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace" + - if pipeline.latest? + %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest + - if pipeline.triggered? + %span.label.label-primary triggered + - if pipeline.yaml_errors.present? + %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid + - if pipeline.builds.any?(&:stuck?) + %span.label.label-warning stuck + + %p.commit-title + - if commit = pipeline.commit + = author_avatar(commit, size: 20) + = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message" + - else + Cant find HEAD commit for this branch + + -# - stages_status = pipeline.statuses.latest.stages_status + -# - stages.each do |stage| + -# %td.stage-cell + -# - status = stages_status[stage] + -# - tooltip = "#{stage.titleize}: #{status || 'not found'}" + -# - if status + -# = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do + -# = ci_icon_for_status(status) + -# - else + -# .light.has-tooltip{ title: tooltip } + -# \- diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 873ed9b59ee..a78407f26ea 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -53,6 +53,10 @@ Commits %span.badge= @commits_count - if @pipeline + %li.pipelines-tab + = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#pipelines', action: 'pipelines', toggle: 'tab'} do + Pipelines + %span.badge= @statuses.size %li.builds-tab = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do Builds @@ -76,6 +80,8 @@ - # This tab is always loaded via AJAX #builds.builds.tab-pane - # This tab is always loaded via AJAX + #pipelines.pipelines.tab-pane + - # This tab is always loaded via AJAX #diffs.diffs.tab-pane - # This tab is always loaded via AJAX diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml index 81de60f116c..808ef7fed27 100644 --- a/app/views/projects/merge_requests/show/_builds.html.haml +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -1,2 +1 @@ = render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true - diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml new file mode 100644 index 00000000000..bcaec137371 --- /dev/null +++ b/app/views/projects/merge_requests/show/_pipelines.html.haml @@ -0,0 +1 @@ += render "projects/commit/pipelines_list", pipeline: @pipeline, link_to_commit: true diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index d9efe81701f..ea618263a4a 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -23,7 +23,8 @@ preparing: "{{status}} build", normal: "Build {{status}}" }, - builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" + builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}", + pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; if (typeof merge_request_widget !== 'undefined') { -- cgit v1.2.3 From d71d3b8c2baaf0527c14d649896590a1e0a9b218 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 25 Jul 2016 16:14:14 -0500 Subject: Convert to javascript --- app/assets/javascripts/merge_request_tabs.js | 24 +- .../javascripts/merge_request_tabs.js.coffee | 266 --------------------- app/assets/javascripts/merge_request_widget.js | 2 +- .../javascripts/merge_request_widget.js.coffee | 143 ----------- 4 files changed, 24 insertions(+), 411 deletions(-) delete mode 100644 app/assets/javascripts/merge_request_tabs.js.coffee delete mode 100644 app/assets/javascripts/merge_request_widget.js.coffee (limited to 'app') diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 52c2ed61012..a21cdbc8a10 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -9,6 +9,8 @@ MergeRequestTabs.prototype.buildsLoaded = false; + MergeRequestTabs.prototype.pipelinesLoaded = false; + MergeRequestTabs.prototype.commitsLoaded = false; function MergeRequestTabs(opts) { @@ -50,6 +52,9 @@ } else if (action === 'builds') { this.loadBuilds($target.attr('href')); this.expandView(); + } else if (action === 'pipelines') { + this.loadPipelines($target.attr('href')); + this.expandView(); } else { this.expandView(); } @@ -81,7 +86,7 @@ if (action === 'show') { action = 'notes'; } - new_state = this._location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, ''); + new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, ''); if (action !== 'notes') { new_state += "/" + action; } @@ -177,6 +182,23 @@ }); }; + MergeRequestTabs.prototype.loadPipelines = function(source) { + if (this.pipelinesLoaded) { + return; + } + return this._get({ + url: source + ".json", + success: (function(_this) { + return function(data) { + document.querySelector("div#pipelines").innerHTML = data.html; + gl.utils.localTimeAgo($('.js-timeago', 'div#pipelines')); + _this.pipelinesLoaded = true; + return _this.scrollToElement("#pipelines"); + }; + })(this) + }); + }; + MergeRequestTabs.prototype.toggleLoading = function(status) { return $('.mr-loading-status .loading').toggle(status); }; diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee deleted file mode 100644 index ccdfcf895a3..00000000000 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ /dev/null @@ -1,266 +0,0 @@ -# MergeRequestTabs -# -# Handles persisting and restoring the current tab selection and lazily-loading -# content on the MergeRequests#show page. -# -#= require jquery.cookie -# -# ### Example Markup -# -# -# -#
-#
-# Notes Content -#
-#
-# Commits Content -#
-#
-# Diffs Content -#
-#
-# -#
-#
-# Loading Animation -#
-#
-# -class @MergeRequestTabs - diffsLoaded: false - buildsLoaded: false - commitsLoaded: false - - constructor: (@opts = {}) -> - # Store the `location` object, allowing for easier stubbing in tests - @_location = location - - @bindEvents() - @activateTab(@opts.action) - - bindEvents: -> - $(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown - $(document).on 'click', '.js-show-tab', @showTab - - showTab: (event) => - event.preventDefault() - - @activateTab $(event.target).data('action') - - tabShown: (event) => - $target = $(event.target) - action = $target.data('action') - - if action == 'commits' - @loadCommits($target.attr('href')) - @expandView() - else if action == 'diffs' - @loadDiff($target.attr('href')) - if bp? and bp.getBreakpointSize() isnt 'lg' - @shrinkView() - - navBarHeight = $('.navbar-gitlab').outerHeight() - $.scrollTo(".merge-request-details .merge-request-tabs", offset: -navBarHeight) - else if action == 'builds' - @loadBuilds($target.attr('href')) - @expandView() - else if action == 'pipelines' - @loadPipelines($target.attr('href')) - @expandView() - else - @expandView() - - @setCurrentAction(action) - - scrollToElement: (container) -> - if window.location.hash - navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() - - $el = $("#{container} #{window.location.hash}:not(.match)") - $.scrollTo("#{container} #{window.location.hash}:not(.match)", offset: -navBarHeight) if $el.length - - # Activate a tab based on the current action - activateTab: (action) -> - action = 'notes' if action == 'show' - $(".merge-request-tabs a[data-action='#{action}']").tab('show') - - # Replaces the current Merge Request-specific action in the URL with a new one - # - # If the action is "notes", the URL is reset to the standard - # `MergeRequests#show` route. - # - # Examples: - # - # location.pathname # => "/namespace/project/merge_requests/1" - # setCurrentAction('diffs') - # location.pathname # => "/namespace/project/merge_requests/1/diffs" - # - # location.pathname # => "/namespace/project/merge_requests/1/diffs" - # setCurrentAction('notes') - # location.pathname # => "/namespace/project/merge_requests/1" - # - # location.pathname # => "/namespace/project/merge_requests/1/diffs" - # setCurrentAction('commits') - # location.pathname # => "/namespace/project/merge_requests/1/commits" - # - # Returns the new URL String - setCurrentAction: (action) => - # Normalize action, just to be safe - action = 'notes' if action == 'show' - - # Remove a trailing '/commits' or '/diffs' - new_state = @_location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '') - - # Append the new action if we're on a tab other than 'notes' - unless action == 'notes' - new_state += "/#{action}" - - # Ensure parameters and hash come along for the ride - new_state += @_location.search + @_location.hash - - # Replace the current history state with the new one without breaking - # Turbolinks' history. - # - # See https://github.com/rails/turbolinks/issues/363 - history.replaceState {turbolinks: true, url: new_state}, document.title, new_state - - new_state - - loadCommits: (source) -> - return if @commitsLoaded - - @_get - url: "#{source}.json" - success: (data) => - document.querySelector("div#commits").innerHTML = data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')) - @commitsLoaded = true - @scrollToElement("#commits") - - loadDiff: (source) -> - return if @diffsLoaded - @_get - url: "#{source}.json" + @_location.search - success: (data) => - $('#diffs').html data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')) - $('#diffs .js-syntax-highlight').syntaxHighlight() - $('#diffs .diff-file').singleFileDiff() - @expandViewContainer() if @diffViewType() is 'parallel' - @diffsLoaded = true - @scrollToElement("#diffs") - @highlighSelectedLine() - @filesCommentButton = $('.files .diff-file').filesCommentButton() - - $(document) - .off 'click', '.diff-line-num a' - .on 'click', '.diff-line-num a', (e) => - e.preventDefault() - window.location.hash = $(e.currentTarget).attr 'href' - @highlighSelectedLine() - @scrollToElement("#diffs") - - highlighSelectedLine: -> - $('.hll').removeClass 'hll' - locationHash = window.location.hash - - if locationHash isnt '' - hashClassString = ".#{locationHash.replace('#', '')}" - $diffLine = $("#{locationHash}:not(.match)", $('#diffs')) - - if not $diffLine.is 'tr' - $diffLine = $('#diffs').find("td#{locationHash}, td#{hashClassString}") - else - $diffLine = $diffLine.find('td') - - if $diffLine.length - $diffLine.addClass 'hll' - diffLineTop = $diffLine.offset().top - navBarHeight = $('.navbar-gitlab').outerHeight() - - loadBuilds: (source) -> - return if @buildsLoaded - - @_get - url: "#{source}.json" - success: (data) => - document.querySelector("div#builds").innerHTML = data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#builds')) - @buildsLoaded = true - @scrollToElement("#builds") - - loadPipelines: (source) -> - return if @pipelinesLoaded - - @_get - url: "#{source}.json" - success: (data) => - document.querySelector("div#pipelines").innerHTML = data.html - gl.utils.localTimeAgo($('.js-timeago', 'div#pipelines')) - @pipelinesLoaded = true - @scrollToElement("#pipelines") - - # Show or hide the loading spinner - # - # status - Boolean, true to show, false to hide - toggleLoading: (status) -> - $('.mr-loading-status .loading').toggle(status) - - _get: (options) -> - defaults = { - beforeSend: => @toggleLoading(true) - complete: => @toggleLoading(false) - dataType: 'json' - type: 'GET' - } - - options = $.extend({}, defaults, options) - - $.ajax(options) - - # Returns diff view type - diffViewType: -> - $('.inline-parallel-buttons a.active').data('view-type') - - expandViewContainer: -> - $('.container-fluid').removeClass('container-limited') - - shrinkView: -> - $gutterIcon = $('.js-sidebar-toggle i:visible') - - # Wait until listeners are set - setTimeout( -> - # Only when sidebar is expanded - if $gutterIcon.is('.fa-angle-double-right') - $gutterIcon.closest('a').trigger('click', [true]) - , 0) - - # Expand the issuable sidebar unless the user explicitly collapsed it - expandView: -> - return if $.cookie('collapsed_gutter') == 'true' - - $gutterIcon = $('.js-sidebar-toggle i:visible') - - # Wait until listeners are set - setTimeout( -> - # Only when sidebar is collapsed - if $gutterIcon.is('.fa-angle-double-left') - $gutterIcon.closest('a').trigger('click', [true]) - , 0) diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js index 362aaa906d0..659bd37c388 100644 --- a/app/assets/javascripts/merge_request_widget.js +++ b/app/assets/javascripts/merge_request_widget.js @@ -28,7 +28,7 @@ MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; - allowedPages = ['show', 'commits', 'builds', 'changes']; + allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; return $(document).on('page:change.merge_request', (function(_this) { return function() { var page; diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee deleted file mode 100644 index 63a12582ddc..00000000000 --- a/app/assets/javascripts/merge_request_widget.js.coffee +++ /dev/null @@ -1,143 +0,0 @@ -class @MergeRequestWidget - # Initialize MergeRequestWidget behavior - # - # check_enable - Boolean, whether to check automerge status - # merge_check_url - String, URL to use to check automerge status - # ci_status_url - String, URL to use to check CI status - # - - constructor: (@opts) -> - $('#modal_merge_info').modal(show: false) - @firstCICheck = true - @readyForCICheck = false - @cancel = false - clearInterval @fetchBuildStatusInterval - - @clearEventListeners() - @addEventListeners() - @getCIStatus(false) - @pollCIStatus() - notifyPermissions() - - clearEventListeners: -> - $(document).off 'page:change.merge_request' - - cancelPolling: -> - @cancel = true - - addEventListeners: -> - allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'] - $(document).on 'page:change.merge_request', => - page = $('body').data('page').split(':').last() - if allowedPages.indexOf(page) < 0 - clearInterval @fetchBuildStatusInterval - @cancelPolling() - @clearEventListeners() - - mergeInProgress: (deleteSourceBranch = false)-> - $.ajax - type: 'GET' - url: $('.merge-request').data('url') - success: (data) => - if data.state == "merged" - urlSuffix = if deleteSourceBranch then '?delete_source=true' else '' - - window.location.href = window.location.pathname + urlSuffix - else if data.merge_error - $('.mr-widget-body').html("

" + data.merge_error + "

") - else - callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch) - setTimeout(callback, 2000) - dataType: 'json' - - getMergeStatus: -> - $.get @opts.merge_check_url, (data) -> - $('.mr-state-widget').replaceWith(data) - - ciLabelForStatus: (status) -> - switch status - when 'success' - 'passed' - when 'success_with_warnings' - 'passed with warnings' - else - status - - pollCIStatus: -> - @fetchBuildStatusInterval = setInterval ( => - return if not @readyForCICheck - - @getCIStatus(true) - - @readyForCICheck = false - ), 10000 - - getCIStatus: (showNotification) -> - _this = @ - $('.ci-widget-fetching').show() - - $.getJSON @opts.ci_status_url, (data) => - return if @cancel - @readyForCICheck = true - - if data.status is '' - return - - if @firstCICheck || data.status isnt @opts.ci_status and data.status? - @opts.ci_status = data.status - @showCIStatus data.status - if data.coverage - @showCICoverage data.coverage - - # The first check should only update the UI, a notification - # should only be displayed on status changes - if showNotification and not @firstCICheck - status = @ciLabelForStatus(data.status) - - if status is "preparing" - title = @opts.ci_title.preparing - status = status.charAt(0).toUpperCase() + status.slice(1); - message = @opts.ci_message.preparing.replace('{{status}}', status) - else - title = @opts.ci_title.normal - message = @opts.ci_message.normal.replace('{{status}}', status) - - title = title.replace('{{status}}', status) - message = message.replace('{{sha}}', data.sha) - message = message.replace('{{title}}', data.title) - - notify( - title, - message, - @opts.gitlab_icon, - -> - @close() - Turbolinks.visit _this.opts.builds_path - ) - @firstCICheck = false - - showCIStatus: (state) -> - return if not state? - $('.ci_widget').hide() - allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"] - if state in allowed_states - $('.ci_widget.ci-' + state).show() - switch state - when "failed", "canceled", "not_found" - @setMergeButtonClass('btn-danger') - when "running" - @setMergeButtonClass('btn-warning') - when "success", "success_with_warnings" - @setMergeButtonClass('btn-create') - else - $('.ci_widget.ci-error').show() - @setMergeButtonClass('btn-danger') - - showCICoverage: (coverage) -> - text = 'Coverage ' + coverage + '%' - $('.ci_widget:visible .ci-coverage').text(text) - - setMergeButtonClass: (css_class) -> - $('.js-merge-button,.accept-action .dropdown-toggle') - .removeClass('btn-danger btn-warning btn-create') - .addClass(css_class) -- cgit v1.2.3 From 46cfd642ec78f9f5f0a174d3f5f6330acb0587dc Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 25 Jul 2016 18:29:53 -0500 Subject: Update MR pipeline view --- app/views/projects/commit/_pipelines_list.haml | 69 +++++----------------- .../merge_requests/show/_pipelines.html.haml | 2 +- 2 files changed, 17 insertions(+), 54 deletions(-) (limited to 'app') diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 2c2feced657..0d6de6dfa2e 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,54 +1,17 @@ -- status = pipeline.status - %ul.content-list.pipelines - - .table-holder - %table.table.builds - %tbody - %th Status - %th Commit - %th.stage - %th - %th - %tr.commit - %td.commit-link - = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do - = ci_status_with_icon(status) - %td - .branch-commit - = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do - %span ##{pipeline.id} - - if pipeline.ref - .icon-container - = pipeline.tag? ? icon('tag') : icon('code-fork') - = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name" - .icon-container - = custom_icon("icon_commit") - = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace" - - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest - - if pipeline.triggered? - %span.label.label-primary triggered - - if pipeline.yaml_errors.present? - %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid - - if pipeline.builds.any?(&:stuck?) - %span.label.label-warning stuck - - %p.commit-title - - if commit = pipeline.commit - = author_avatar(commit, size: 20) - = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message" - - else - Cant find HEAD commit for this branch - - -# - stages_status = pipeline.statuses.latest.stages_status - -# - stages.each do |stage| - -# %td.stage-cell - -# - status = stages_status[stage] - -# - tooltip = "#{stage.titleize}: #{status || 'not found'}" - -# - if status - -# = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do - -# = ci_icon_for_status(status) - -# - else - -# .light.has-tooltip{ title: tooltip } - -# \- + - if pipelines.blank? + %li + .nothing-here-block No pipelines to show + - else + .table-holder + %table.table.builds + %tbody + %th Status + %th Commit + - pipelines.stages.each do |stage| + %th.stage + %span.has-tooltip{ title: "#{stage.titleize}" } + = stage.titleize + %th + %th + = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml index bcaec137371..afe3f3430c6 100644 --- a/app/views/projects/merge_requests/show/_pipelines.html.haml +++ b/app/views/projects/merge_requests/show/_pipelines.html.haml @@ -1 +1 @@ -= render "projects/commit/pipelines_list", pipeline: @pipeline, link_to_commit: true += render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true -- cgit v1.2.3 From 0d6d7f6e30ee24a82dfede92d0ebbda86d93edb5 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 25 Jul 2016 19:30:52 -0500 Subject: Display all pipelines for Merge Request --- app/controllers/projects/merge_requests_controller.rb | 2 ++ app/views/projects/commit/_pipelines_list.haml | 2 +- app/views/projects/merge_requests/_show.html.haml | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 5c6396fba9f..acdb5ba5c4f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -137,6 +137,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def pipelines + @pipelines = Ci::Pipeline.where(ref: @merge_request.source_branch) + respond_to do |format| format.html do define_discussion_vars diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 0d6de6dfa2e..03db765180a 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -14,4 +14,4 @@ = stage.titleize %th %th - = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages + = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index a78407f26ea..9ae2b52bace 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -56,7 +56,7 @@ %li.pipelines-tab = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#pipelines', action: 'pipelines', toggle: 'tab'} do Pipelines - %span.badge= @statuses.size + %span.badge %li.builds-tab = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do Builds -- cgit v1.2.3 From 94f22826f30692f693dbd8f6ccdea91f4dba0bbb Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Mon, 25 Jul 2016 22:03:07 -0600 Subject: Don't show comment button in gutter of diffs on MR discussion tab --- app/assets/javascripts/files_comment_button.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js index 09b5eb398d4..b2e49b71fec 100644 --- a/app/assets/javascripts/files_comment_button.js +++ b/app/assets/javascripts/files_comment_button.js @@ -33,7 +33,7 @@ this.render = bind(this.render, this); this.VIEW_TYPE = $('input#view[type=hidden]').val(); debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); - $(document).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); + $(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); } FilesCommentButton.prototype.render = function(e) { -- cgit v1.2.3 From 105e51c1dc774f443a9097d6b4acbfae4047403c Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 26 Jul 2016 14:25:42 -0500 Subject: Line tooltip up with icon --- app/assets/stylesheets/pages/commits.scss | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app') diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 0298577c494..cbc980f52ff 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -61,6 +61,10 @@ font-size: 0; } + .ci-status-link { + display: inline-block; + } + .btn-clipboard, .btn-transparent { padding-left: 0; padding-right: 0; -- cgit v1.2.3 From df0a77a7bed7c860793e027421a3fafeb12646df Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Tue, 26 Jul 2016 16:59:55 -0600 Subject: Removes two simple instances of inline JavaScript. --- app/assets/javascripts/dispatcher.js | 4 ++++ app/views/admin/labels/_form.html.haml | 3 --- app/views/projects/_home_panel.html.haml | 3 --- 3 files changed, 4 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index d212d66da1b..308885dcfe9 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -181,6 +181,9 @@ break; case 'projects': new NamespaceSelects(); + break; + case 'labels': + new Labels(); } break; case 'dashboard': @@ -206,6 +209,7 @@ new ProjectNew(); break; case 'show': + new Star(); new ProjectNew(); new ProjectShow(); new NotificationsDropdown(); diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index 448aa953548..602cfa9b6fc 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -28,6 +28,3 @@ .form-actions = f.submit 'Save', class: 'btn btn-save js-save-button' = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel' - -:javascript - new Labels(); diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 51f74f3b7ce..8ef31ca3bda 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -24,6 +24,3 @@ .project-clone-holder = render "shared/clone_panel" - -:javascript - new Star(); -- cgit v1.2.3 From 8f3e3f6b8070d6887f0d267fcaea89579d909287 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 27 Jul 2016 03:24:11 -0500 Subject: Make sure Labels is instantiated on edit page --- app/assets/javascripts/dispatcher.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 308885dcfe9..82435c51a0b 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -183,7 +183,10 @@ new NamespaceSelects(); break; case 'labels': - new Labels(); + switch (path[2]) { + case 'edit': + new Labels(); + } } break; case 'dashboard': -- cgit v1.2.3 From 6e50719ff8dde5b5d54eea5e8f6fa95c5d0c0c24 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 21 Jul 2016 17:16:23 +0100 Subject: Fixed artifacts expire date in FF --- app/assets/javascripts/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index e135cb92a30..3d9b824d406 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -128,7 +128,7 @@ $date = $('.js-artifacts-remove'); if ($date.length) { date = $date.text(); - return $date.text($.timefor(new Date(date), ' ')); + return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' ')); } }; -- cgit v1.2.3 From 33e14285d3dc0c2340ca34ede7235644a418cea6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 27 Jul 2016 13:08:43 +0200 Subject: Add generic SVG badge template --- app/views/projects/badges/badge.svg.erb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/views/projects/badges/badge.svg.erb (limited to 'app') diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb new file mode 100644 index 00000000000..72f341176e3 --- /dev/null +++ b/app/views/projects/badges/badge.svg.erb @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + build + build + success + success + + + -- cgit v1.2.3 From 42c035ee38a6629090da07aea818f4c9e3733075 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Jul 2016 13:50:50 +0200 Subject: Add badge template to generic badge view template --- app/views/projects/badges/badge.svg.erb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) (limited to 'app') diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb index 72f341176e3..5a71419d3af 100644 --- a/app/views/projects/badges/badge.svg.erb +++ b/app/views/projects/badges/badge.svg.erb @@ -1,25 +1,25 @@ - + - + - - - + + + - build - build - success - success + <%= badge.key_text %> + <%= badge.key_text %> + <%= badge.value_text %> + <%= badge.value_text %> -- cgit v1.2.3 From 9ae1ecf876e40ce9dd64c72e025f32e38c882fd6 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Jul 2016 14:35:02 +0200 Subject: Extract build status badge metadata to separate class --- app/controllers/projects/pipelines_settings_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 85ba706e5cd..75dd3648e45 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -3,7 +3,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController def show @ref = params[:ref] || @project.default_branch || 'master' - @build_badge = Gitlab::Badge::Build.new(@project, @ref) + @build_badge = Gitlab::Badge::Build.new(@project, @ref).metadata end def update -- cgit v1.2.3 From d49e6f355010a31a080da75789b46603989925ad Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 27 Jul 2016 11:51:52 -0500 Subject: Add pipeline icon to admin builds; position warning icon after sha --- app/views/admin/builds/_build.html.haml | 7 ++++--- app/views/projects/ci/builds/_build.html.haml | 13 +++++-------- 2 files changed, 9 insertions(+), 11 deletions(-) (limited to 'app') diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index ce818c30c30..6d1ccf32b56 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -11,16 +11,17 @@ - else %span.build-link ##{build.id} - - if build.stuck? - %i.fa.fa-warning.text-warning - - if build.ref + .icon-container + = build.tag? ? icon('tag') : icon('code-fork') = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - else .light none = custom_icon("icon_commit") = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id" + - if build.stuck? + %i.fa.fa-warning.text-warning .label-container - if build.tags.any? diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index a3114771a42..91081435220 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -13,13 +13,6 @@ - else %span ##{build.id} - - if build.stuck? - .icon-container - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') - - if defined?(retried) && retried - .icon-container - = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') - - if defined?(ref) && ref - if build.ref .icon-container @@ -33,6 +26,11 @@ - if defined?(commit_sha) && commit_sha = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" + - if build.stuck? + = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + - if defined?(retried) && retried + = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.') + .label-container - if build.tags.any? - build.tags.each do |tag| @@ -47,7 +45,6 @@ - if build.manual? %span.label.label-info manual - - if defined?(runner) && runner %td - if build.try(:runner) -- cgit v1.2.3 From d286c624defd0549642c787ccd6782ee3e5ff42f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 27 Jul 2016 12:59:35 -0500 Subject: Adjust min-widths of pipelines and builds tables --- app/assets/stylesheets/pages/pipelines.scss | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index c58e2ffe7f5..7411c1c4499 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -18,6 +18,10 @@ .btn { margin: 4px; } + + .table.builds { + min-width: 1200px; + } } .content-list { @@ -35,7 +39,7 @@ } .table.builds { - min-width: 1200px; + min-width: 900px; &.pipeline { min-width: 650px; -- cgit v1.2.3 From 59797acd85037e9f491d298ac37743897ca2d6c2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 27 Jul 2016 13:29:32 -0500 Subject: Add icon container --- app/views/admin/builds/_build.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml index 6d1ccf32b56..352adbedee4 100644 --- a/app/views/admin/builds/_build.html.haml +++ b/app/views/admin/builds/_build.html.haml @@ -17,7 +17,8 @@ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name" - else .light none - = custom_icon("icon_commit") + .icon-container + = custom_icon("icon_commit") = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "monospace commit-id" - if build.stuck? -- cgit v1.2.3 From cebda439ea88d9868902120b1d20ff304610d9fd Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Wed, 27 Jul 2016 17:31:53 -0500 Subject: Decrease icon container width to help fit all pipeline commit info on two lines --- app/assets/stylesheets/pages/pipelines.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 7411c1c4499..21919fe4d73 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -132,7 +132,7 @@ .icon-container { display: inline-block; text-align: right; - width: 20px; + width: 15px; .fa { position: relative; -- cgit v1.2.3 From 3f1422a629a6866bc4cf164c00479eedb891ef8a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 28 Jul 2016 11:48:14 -0500 Subject: Add CI configuration button on project page --- app/views/projects/show.html.haml | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app') diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index dd1cf680cfa..cd0cd923ddb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -43,6 +43,10 @@ %li = link_to 'Contribution guide', contribution_guide_path(@project) + - if @repository.gitlab_ci_yml + %li + = link_to 'CI configuration', project_environments_path(@project) + - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog %li.missing -- cgit v1.2.3 From afca25ed8abddf4f9f3767fbc26a990a6e61dc8b Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Thu, 28 Jul 2016 12:06:15 -0500 Subject: Link configuration button to .gitlab-ci.yml --- app/helpers/projects_helper.rb | 4 ++++ app/views/projects/show.html.haml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index a733dff1579..505545fbabb 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -263,6 +263,10 @@ module ProjectsHelper filename_path(project, :version) end + def ci_configuration_path(project) + filename_path(project, :gitlab_ci_yml) + end + def project_wiki_path_with_version(proj, page, version, is_newest) url_params = is_newest ? {} : { version_id: version } namespace_project_wiki_path(proj.namespace, proj, page, url_params) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index cd0cd923ddb..a666d07e9eb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -45,7 +45,7 @@ - if @repository.gitlab_ci_yml %li - = link_to 'CI configuration', project_environments_path(@project) + = link_to 'CI configuration', ci_configuration_path(@project) - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog -- cgit v1.2.3 From be9aa7f19474424991923f128053e2523fa166d8 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 26 Jul 2016 09:35:47 +0200 Subject: Add an URL field to Environments This MR adds a string (thus max 255 chars) field to the enviroments table to expose it later in other features. --- .../projects/environments_controller.rb | 23 ++++++++++++----- app/models/environment.rb | 4 +++ app/views/projects/environments/_form.html.haml | 29 ++++++++++++++++------ app/views/projects/environments/edit.html.haml | 6 +++++ app/views/projects/environments/new.html.haml | 14 +++-------- app/views/projects/environments/show.html.haml | 2 +- 6 files changed, 54 insertions(+), 24 deletions(-) create mode 100644 app/views/projects/environments/edit.html.haml (limited to 'app') diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 4b433796161..1f5c7506212 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,8 +2,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_update_environment!, only: [:destroy] - before_action :environment, only: [:show, :destroy] + before_action :authorize_update_environment!, only: [:edit, :destroy] + before_action :environment, only: [:show, :edit, :update, :destroy] def index @environments = project.environments @@ -17,13 +17,24 @@ class Projects::EnvironmentsController < Projects::ApplicationController @environment = project.environments.new end + def edit + end + def create - @environment = project.environments.create(create_params) + @environment = project.environments.create(environment_params) if @environment.persisted? redirect_to namespace_project_environment_path(project.namespace, project, @environment) else - render 'new' + render :new + end + end + + def update + if @environment.update(environment_params) + redirect_to namespace_project_environment_path(project.namespace, project, @environment) + else + render :edit end end @@ -39,8 +50,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController private - def create_params - params.require(:environment).permit(:name) + def environment_params + params.require(:environment).permit(:name, :external_url) end def environment diff --git a/app/models/environment.rb b/app/models/environment.rb index ac3a571a1f3..9eff0fdab03 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -10,6 +10,10 @@ class Environment < ActiveRecord::Base format: { with: Gitlab::Regex.environment_name_regex, message: Gitlab::Regex.environment_name_regex_message } + validates :external_url, + uniqueness: { scope: :project_id }, + length: { maximum: 255 } + def last_deployment deployments.last end diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml index c07f4bd510c..6d040f5cfe6 100644 --- a/app/views/projects/environments/_form.html.haml +++ b/app/views/projects/environments/_form.html.haml @@ -1,7 +1,22 @@ -= form_for @environment, url: namespace_project_environments_path(@project.namespace, @project), html: { class: 'col-lg-9' } do |f| - = form_errors(@environment) - .form-group - = f.label :name, 'Name', class: 'label-light' - = f.text_field :name, required: true, class: 'form-control' - = f.submit 'Create environment', class: 'btn btn-create' - = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel' +.row.prepend-top-default.append-bottom-default + .col-lg-3 + %h4.prepend-top-0 + Environments + %p + Environments allow you to track deployments of your application + = succeed "." do + = link_to "Read more about environments", help_page_path("ci/environments") + + = form_for [@project.namespace.becomes(Namespace), @project, @environment], html: { class: 'col-lg-9' } do |f| + = form_errors(@environment) + + .form-group + = f.label :name, 'Name', class: 'label-light' + = f.text_field :name, required: true, class: 'form-control' + .form-group + = f.label :external_url, 'External URL', class: 'label-light' + = f.url_field :external_url, class: 'form-control' + + .form-actions + = f.submit 'Save', class: 'btn btn-save' + = link_to 'Cancel', namespace_project_environments_path(@project.namespace, @project), class: 'btn btn-cancel' diff --git a/app/views/projects/environments/edit.html.haml b/app/views/projects/environments/edit.html.haml new file mode 100644 index 00000000000..6d1bdb9320f --- /dev/null +++ b/app/views/projects/environments/edit.html.haml @@ -0,0 +1,6 @@ +- page_title "Edit", @environment.name, "Environments" + +%h3.page-title + Edit environment +%hr += render 'form' diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml index 89e06567196..e51667ade2d 100644 --- a/app/views/projects/environments/new.html.haml +++ b/app/views/projects/environments/new.html.haml @@ -1,12 +1,6 @@ - page_title 'New Environment' -.row.prepend-top-default.append-bottom-default - .col-lg-3 - %h4.prepend-top-0 - New Environment - %p - Environments allow you to track deployments of your application - = succeed "." do - = link_to "Read more about environments", help_page_path("ci/environments") - - = render 'form' +%h3.page-title + New environment +%hr += render 'form' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index b8b1ce52a91..a07436ad7c9 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -6,10 +6,10 @@ .top-area .col-md-9 %h3.page-title= @environment.name.capitalize - .col-md-3 .nav-controls - if can?(current_user, :update_environment, @environment) + = link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn' = link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete - if @deployments.blank? -- cgit v1.2.3 From 76e9b68439510af5c783a81b93944f1c8d96d150 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 26 Jul 2016 14:19:37 +0200 Subject: Incorporate feedback --- app/models/environment.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/environment.rb b/app/models/environment.rb index 9eff0fdab03..baed106e8c8 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -3,6 +3,8 @@ class Environment < ActiveRecord::Base has_many :deployments + before_validation :nullify_external_url + validates :name, presence: true, uniqueness: { scope: :project_id }, @@ -12,9 +14,15 @@ class Environment < ActiveRecord::Base validates :external_url, uniqueness: { scope: :project_id }, - length: { maximum: 255 } + length: { maximum: 255 }, + allow_nil: true, + addressable_url: true def last_deployment deployments.last end + + def nullify_external_url + self.external_url = nil if self.external_url.blank? + end end -- cgit v1.2.3 From a54419f01f6cad33138e3d4ed049741699251f85 Mon Sep 17 00:00:00 2001 From: winniehell Date: Sun, 24 Jul 2016 07:32:47 +0200 Subject: Make "New issue" button in Issue page less obtrusive (!5457) --- app/views/help/ui.html.haml | 2 +- app/views/projects/issues/show.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index 431d312b4ca..85e188d6f8b 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -189,7 +189,7 @@ %li %a Sort by date - = link_to 'New issue', '#', class: 'btn btn-new' + = link_to 'New issue', '#', class: 'btn btn-new btn-inverted' .lead Only nav links without button and search diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 9b6a97c0959..e5cce16a171 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -38,7 +38,7 @@ %li = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) - if can?(current_user, :create_issue, @project) - = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-success', title: 'New issue', id: 'new_issue_link' do + = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do New issue - if can?(current_user, :update_issue, @issue) = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' -- cgit v1.2.3 From 5b4ceeed6317cc8039642981ba356565e11d991e Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Fri, 29 Jul 2016 18:24:11 -0300 Subject: Fix attr reader to force the intended values for source and target shas When importing a pull request from GitHub, the old and new branches may no longer actually exist by those names, but we need to recreate the merge request diff with the right source and target shas. We use these `target_branch_sha` and `source_branch_sha` attributes to force these to the intended values. But the reader methods were always looking up to the target/source branch head instead of check if these values was previously set. --- app/models/merge_request.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app') diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 471e32f3b60..fdcbbdc1d08 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -238,10 +238,14 @@ class MergeRequest < ActiveRecord::Base end def target_branch_sha + return @target_branch_sha if defined?(@target_branch_sha) + target_branch_head.try(:sha) end def source_branch_sha + return @source_branch_sha if defined?(@source_branch_sha) + source_branch_head.try(:sha) end -- cgit v1.2.3 From 285ba1b20f226f0bf7ab01010b64cabdccecf096 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Sun, 31 Jul 2016 19:44:02 -0300 Subject: fixup! Fix attr reader to force the intended values for source and target shas --- app/models/merge_request.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index fdcbbdc1d08..f1b9c1d6feb 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -238,15 +238,11 @@ class MergeRequest < ActiveRecord::Base end def target_branch_sha - return @target_branch_sha if defined?(@target_branch_sha) - - target_branch_head.try(:sha) + @target_branch_sha || target_branch_head.try(:sha) end def source_branch_sha - return @source_branch_sha if defined?(@source_branch_sha) - - source_branch_head.try(:sha) + @source_branch_sha || source_branch_head.try(:sha) end def diff_refs -- cgit v1.2.3 From 34c1c8a3b14ab3b29fbde97532c89404d9573a1d Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 1 Aug 2016 08:42:09 +0200 Subject: Minor fixes in the Env API endpoints --- app/controllers/projects/environments_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 1f5c7506212..58678f96879 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_update_environment!, only: [:edit, :destroy] + before_action :authorize_update_environment!, only: [:edit, :update, :destroy] before_action :environment, only: [:show, :edit, :update, :destroy] def index -- cgit v1.2.3 From aad0ae71620d8e988faf75587a612b933df00366 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Fri, 22 Jul 2016 12:10:45 +0200 Subject: squashed - fixed label and milestone association problems, updated specs and refactored reader class a bit --- app/models/label_link.rb | 6 ++++-- app/models/project.rb | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/label_link.rb b/app/models/label_link.rb index 47bd6eaf35f..51b5c2b1f4c 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -1,7 +1,9 @@ class LabelLink < ActiveRecord::Base + include Importable + belongs_to :target, polymorphic: true belongs_to :label - validates :target, presence: true - validates :label, presence: true + validates :target, presence: true, unless: :importing? + validates :label, presence: true, unless: :importing? end diff --git a/app/models/project.rb b/app/models/project.rb index 7aecd7860c5..83b848ded8b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1253,6 +1253,16 @@ class Project < ActiveRecord::Base authorized_for_user_by_shared_projects?(user, min_access_level) end + def append_or_update_attribute(name, value) + old_values = public_send(name.to_s) + + if Project.reflect_on_association(name).try(:macro) == :has_many && old_values.any? + update_attribute(name, old_values + value) + else + update_attribute(name, value) + end + end + private def authorized_for_user_by_group?(user, min_access_level) -- cgit v1.2.3 From 84a3225b0cde0ed2e343864583e7b79d7118e05c Mon Sep 17 00:00:00 2001 From: zs Date: Sun, 24 Jul 2016 01:28:12 +0200 Subject: State specific default sort order for issuables Provide more sensible default sort order for issues and merge requests based on the following table: | type | state | default sort order | |----------------|--------|--------------------| | issues | open | last created | | issues | closed | last updated | | issues | all | last created | | merge requests | open | last created | | merge requests | merged | last updated | | merge requests | closed | last updated | | merge requests | all | last created | --- app/controllers/application_controller.rb | 56 --------------- app/controllers/concerns/issuable_collections.rb | 79 ++++++++++++++++++++++ app/controllers/concerns/issues_action.rb | 10 +-- app/controllers/concerns/merge_requests_action.rb | 10 +-- app/controllers/projects/issues_controller.rb | 3 +- .../projects/merge_requests_controller.rb | 3 +- app/finders/issuable_finder.rb | 2 +- app/helpers/application_helper.rb | 1 - app/helpers/sorting_helper.rb | 4 +- app/views/projects/issues/index.html.haml | 8 ++- 10 files changed, 105 insertions(+), 71 deletions(-) create mode 100644 app/controllers/concerns/issuable_collections.rb (limited to 'app') diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a1004d9bcea..634d36a4467 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -243,42 +243,6 @@ class ApplicationController < ActionController::Base end end - def set_filters_params - set_default_sort - - params[:scope] = 'all' if params[:scope].blank? - params[:state] = 'opened' if params[:state].blank? - - @sort = params[:sort] - @filter_params = params.dup - - if @project - @filter_params[:project_id] = @project.id - elsif @group - @filter_params[:group_id] = @group.id - else - # TODO: this filter ignore issues/mr created in public or - # internal repos where you are not a member. Enable this filter - # or improve current implementation to filter only issues you - # created or assigned or mentioned - # @filter_params[:authorized_only] = true - end - - @filter_params - end - - def get_issues_collection - set_filters_params - @issuable_finder = IssuesFinder.new(current_user, @filter_params) - @issuable_finder.execute - end - - def get_merge_requests_collection - set_filters_params - @issuable_finder = MergeRequestsFinder.new(current_user, @filter_params) - @issuable_finder.execute - end - def import_sources_enabled? !current_application_settings.import_sources.empty? end @@ -363,24 +327,4 @@ class ApplicationController < ActionController::Base def u2f_app_id request.base_url end - - private - - def set_default_sort - key = if is_a_listing_page_for?('issues') || is_a_listing_page_for?('merge_requests') - 'issuable_sort' - end - - cookies[key] = params[:sort] if key && params[:sort].present? - params[:sort] = cookies[key] if key - params[:sort] ||= 'id_desc' - end - - def is_a_listing_page_for?(page_type) - controller_name, action_name = params.values_at(:controller, :action) - - (controller_name == "projects/#{page_type}" && action_name == 'index') || - (controller_name == 'groups' && action_name == page_type) || - (controller_name == 'dashboard' && action_name == page_type) - end end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb new file mode 100644 index 00000000000..c802922e0af --- /dev/null +++ b/app/controllers/concerns/issuable_collections.rb @@ -0,0 +1,79 @@ +module IssuableCollections + extend ActiveSupport::Concern + include SortingHelper + + included do + helper_method :issues_finder + helper_method :merge_requests_finder + end + + private + + def issues_collection + issues_finder.execute + end + + def merge_requests_collection + merge_requests_finder.execute + end + + def issues_finder + @issues_finder ||= issuable_finder_for(IssuesFinder) + end + + def merge_requests_finder + @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) + end + + def issuable_finder_for(finder_class) + finder_class.new(current_user, filter_params) + end + + def filter_params + set_sort_order_from_cookie + set_default_scope + set_default_state + + @filter_params = params.dup + @filter_params[:sort] ||= default_sort_order + + @sort = @filter_params[:sort] + + if @project + @filter_params[:project_id] = @project.id + elsif @group + @filter_params[:group_id] = @group.id + else + # TODO: this filter ignore issues/mr created in public or + # internal repos where you are not a member. Enable this filter + # or improve current implementation to filter only issues you + # created or assigned or mentioned + # @filter_params[:authorized_only] = true + end + + @filter_params + end + + def set_default_scope + params[:scope] = 'all' if params[:scope].blank? + end + + def set_default_state + params[:state] = 'opened' if params[:state].blank? + end + + def set_sort_order_from_cookie + key = 'issuable_sort' + + cookies[key] = params[:sort] if params[:sort].present? + params[:sort] = cookies[key] + end + + def default_sort_order + case params[:state] + when 'opened', 'all' then sort_value_recently_created + when 'merged', 'closed' then sort_value_recently_updated + else sort_value_recently_created + end + end +end diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index 4feabc32b1c..b89fb94be6e 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -1,12 +1,14 @@ module IssuesAction extend ActiveSupport::Concern + include IssuableCollections def issues - @issues = get_issues_collection.non_archived - @issues = @issues.page(params[:page]) - @issues = @issues.preload(:author, :project) + @label = issues_finder.labels.first - @label = @issuable_finder.labels.first + @issues = issues_collection + .non_archived + .preload(:author, :project) + .page(params[:page]) respond_to do |format| format.html diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 06a6b065e7e..a1b0eee37f9 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -1,11 +1,13 @@ module MergeRequestsAction extend ActiveSupport::Concern + include IssuableCollections def merge_requests - @merge_requests = get_merge_requests_collection.non_archived - @merge_requests = @merge_requests.page(params[:page]) - @merge_requests = @merge_requests.preload(:author, :target_project) + @label = merge_requests_finder.labels.first - @label = @issuable_finder.labels.first + @merge_requests = merge_requests_collection + .non_archived + .preload(:author, :target_project) + .page(params[:page]) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 3c6f29ac0ba..7f5c3ff3d6a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -3,6 +3,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleSubscriptionAction include IssuableActions include ToggleAwardEmoji + include IssuableCollections before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, @@ -24,7 +25,7 @@ class Projects::IssuesController < Projects::ApplicationController def index terms = params['issue_search'] - @issues = get_issues_collection + @issues = issues_collection if terms.present? if terms =~ /\A#(\d+)\z/ diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 47c21a18b33..03166294ddd 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -5,6 +5,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController include IssuableActions include NotesHelper include ToggleAwardEmoji + include IssuableCollections before_action :module_enabled before_action :merge_request, only: [ @@ -29,7 +30,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def index terms = params['issue_search'] - @merge_requests = get_merge_requests_collection + @merge_requests = merge_requests_collection if terms.present? if terms =~ /\A[#!](\d+)\z/ diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index a0932712bd0..33daac0399e 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -109,7 +109,7 @@ class IssuableFinder scope.where(title: params[:milestone_title]) else - nil + Milestone.none end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 03495cf5ec4..50de93d4bdf 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -245,7 +245,6 @@ module ApplicationHelper milestone_title: params[:milestone_title], assignee_id: params[:assignee_id], author_id: params[:author_id], - sort: params[:sort], issue_search: params[:issue_search], label_name: params[:label_name] } diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index d86f1999f5c..e1c0b497550 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -102,11 +102,11 @@ module SortingHelper end def sort_value_oldest_created - 'id_asc' + 'created_asc' end def sort_value_recently_created - 'id_desc' + 'created_desc' end def sort_value_milestone_soon diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml index d0edd2f22ec..1a87045aa60 100644 --- a/app/views/projects/issues/index.html.haml +++ b/app/views/projects/issues/index.html.haml @@ -19,7 +19,13 @@ 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 + = link_to new_namespace_project_issue_path(@project.namespace, + @project, + issue: { assignee_id: issues_finder.assignee.try(:id), + milestone_id: issues_finder.milestones.first.try(:id) }), + class: "btn btn-new", + title: "New Issue", + id: "new_issue_link" do New Issue = render 'shared/issuable/filter', type: :issues -- cgit v1.2.3 From 44eec823fb68238705eb932dd14aa211b730a316 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Mon, 1 Aug 2016 12:29:40 +0200 Subject: Avoid line_code and position calculation on line partial for plain view --- app/views/projects/diffs/_line.html.haml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 5a8a131d10c..4d3af905b58 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -1,8 +1,7 @@ - plain = local_assigns.fetch(:plain, false) -- line_code = diff_file.line_code(line) -- position = diff_file.position(line) - type = line.type -%tr.line_holder{ id: line_code, class: type } +- line_code = diff_file.line_code(line) unless plain +%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } } - case type - when 'match' = render "projects/diffs/match_line", { line: line.text, @@ -24,4 +23,4 @@ = link_text - else %a{href: "##{line_code}", data: { linenumber: link_text }} - %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, position, type) unless plain) }= diff_line_content(line.text, type) + %td.line_content.noteable_line{ class: type, data: (diff_view_line_data(line_code, diff_file.position(line), type) unless plain) }= diff_line_content(line.text, type) -- cgit v1.2.3 From 9845079950da010b8a4c07777f984aaf02642ad0 Mon Sep 17 00:00:00 2001 From: Felipe Artur Date: Fri, 29 Jul 2016 16:20:00 -0300 Subject: Fix search results for notes without commits --- app/views/search/results/_note.html.haml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index 8163aff43b6..e0400083870 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -1,6 +1,7 @@ - project = note.project - note_url = Gitlab::UrlBuilder.build(note) -- noteable_identifier = note.noteable.try(:iid) || note.noteable.id +- noteable_identifier = note.noteable.try(:iid) || note.noteable.try(:id) + .search-result-row %h5.note-search-caption.str-truncated %i.fa.fa-comment @@ -10,7 +11,10 @@ · - if note.for_commit? - = link_to "Commit #{truncate_sha(note.commit_id)}", note_url + = link_to_if(noteable_identifier, "Commit #{truncate_sha(note.commit_id)}", note_url) do + = truncate_sha(note.commit_id) + %span.light Commit deleted + - else %span #{note.noteable_type.titleize} ##{noteable_identifier} · -- cgit v1.2.3 From af7ce322bdbaf74eaf54eac92c2ed5183e0d8e9c Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Wed, 27 Jul 2016 15:39:45 -0400 Subject: webhooks: include old revision in MR update events --- app/services/merge_requests/base_service.rb | 9 ++++++--- app/services/merge_requests/refresh_service.rb | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index bc3606a14c2..ba424b09463 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -17,16 +17,19 @@ module MergeRequests end end - def hook_data(merge_request, action) + def hook_data(merge_request, action, oldrev = nil) hook_data = merge_request.to_hook_data(current_user) hook_data[:object_attributes][:url] = Gitlab::UrlBuilder.build(merge_request) hook_data[:object_attributes][:action] = action + if oldrev && !Gitlab::Git.blank_ref?(oldrev) + hook_data[:object_attributes][:oldrev] = oldrev + end hook_data end - def execute_hooks(merge_request, action = 'open') + def execute_hooks(merge_request, action = 'open', oldrev = nil) if merge_request.project - merge_data = hook_data(merge_request, action) + merge_data = hook_data(merge_request, action, oldrev) merge_request.project.execute_hooks(merge_data, :merge_request_hooks) merge_request.project.execute_services(merge_data, :merge_request_hooks) end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 1daf6bbf553..5cedd6f11d9 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -137,7 +137,7 @@ module MergeRequests # Call merge request webhook with update branches def execute_mr_web_hooks merge_requests_for_source_branch.each do |merge_request| - execute_hooks(merge_request, 'update') + execute_hooks(merge_request, 'update', @oldrev) end end -- cgit v1.2.3 From d61b92a350f50fa1a443f0a180da401684f5cdca Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 1 Aug 2016 12:20:38 -0500 Subject: Convert image diff background image to CSS --- app/assets/images/trans_bg.gif | Bin 49 -> 0 bytes app/assets/stylesheets/pages/diff.scss | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) delete mode 100644 app/assets/images/trans_bg.gif (limited to 'app') diff --git a/app/assets/images/trans_bg.gif b/app/assets/images/trans_bg.gif deleted file mode 100644 index 1a1c9c15ec7..00000000000 Binary files a/app/assets/images/trans_bg.gif and /dev/null differ diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 21b1c223c88..21cee2e3a70 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -164,7 +164,10 @@ line-height: 0; img { border: 1px solid #fff; - background: image-url('trans_bg.gif'); + background-image: linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%), + linear-gradient(45deg, #e5e5e5 25%, transparent 25%, transparent 75%, #e5e5e5 75%, #e5e5e5 100%); + background-size: 10px 10px; + background-position: 0 0, 5px 5px; max-width: 100%; } &.deleted { -- cgit v1.2.3 From fe23512c9d0cc8348cb4221fc110b53748a6996b Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 1 Aug 2016 15:09:03 -0500 Subject: Convert switch icon into icon font --- app/assets/images/switch_icon.png | Bin 231 -> 0 bytes app/assets/stylesheets/pages/commits.scss | 2 -- app/views/projects/compare/_form.html.haml | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 app/assets/images/switch_icon.png (limited to 'app') diff --git a/app/assets/images/switch_icon.png b/app/assets/images/switch_icon.png deleted file mode 100644 index c6b6c8d9521..00000000000 Binary files a/app/assets/images/switch_icon.png and /dev/null differ diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index 0298577c494..2beef15bbf9 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -1,8 +1,6 @@ .commits-compare-switch { @include btn-default; @include btn-white; - background: image-url("switch_icon.png") no-repeat center center; - text-indent: -9999px; float: left; margin-right: 9px; } diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index af09b3418ea..d79336f5a60 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -1,7 +1,7 @@ = form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do .clearfix - if params[:to] && params[:from] - = link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} + = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'} .form-group.dropdown.compare-form-group.js-compare-from-dropdown .input-group.inline-input-group %span.input-group-addon from -- cgit v1.2.3 From 99c02ed53c994fbd71442410c78daf220c6d1ced Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Mon, 1 Aug 2016 13:11:45 -0700 Subject: Only use RequestStore in ProjectTeam#max_member_access_for_user if it is active --- app/models/project_team.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/project_team.rb b/app/models/project_team.rb index fdfaf052730..19fd082534c 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -138,8 +138,13 @@ class ProjectTeam def max_member_access_for_user_ids(user_ids) user_ids = user_ids.uniq key = "max_member_access:#{project.id}" - RequestStore.store[key] ||= {} - access = RequestStore.store[key] + + access = {} + + if RequestStore.active? + RequestStore.store[key] ||= {} + access = RequestStore.store[key] + end # Lookup only the IDs we need user_ids = user_ids - access.keys -- cgit v1.2.3 From 3f2f4bda456a6e04aca6ab27983b337ddfdef3c5 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Sun, 31 Jul 2016 18:37:15 -0700 Subject: Remove delay when hitting Reply... button on page with a lot of discussions --- app/assets/javascripts/gfm_auto_complete.js | 4 ++-- app/assets/javascripts/gl_form.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 41f4c1914f2..2e5b15f4b77 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -47,8 +47,8 @@ } } }, - setup: function(wrap) { - this.input = $('.js-gfm-input'); + setup: function(input) { + this.input = input || $('.js-gfm-input'); this.destroyAtWho(); this.setupAtWho(); if (this.dataSource) { diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 6ac7564a848..528a673eb15 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -21,7 +21,7 @@ this.form.find('.div-dropzone').remove(); this.form.addClass('gfm-form'); disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button')); - GitLab.GfmAutoComplete.setup(); + GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); new DropzoneInput(this.form); autosize(this.textarea); this.addEventListeners(); -- cgit v1.2.3 From a70431f874112212cb44b7a104b2e32f440af941 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 1 Aug 2016 16:59:44 -0700 Subject: Redirect to external issue tracker from `/issues` Prior, in order to display the correct link to "Issues" in the project navigation, we were performing a check against the project to see if it used an external issue tracker, and if so, we used that URL. This was inefficient. Now, we simply _always_ link to `namespace_project_issues_path`, and then in the controller we redirect to the external tracker if it's present. This also removes the need for the url_for_issue helper. Bonus! :tada: --- app/controllers/projects/issues_controller.rb | 7 +++++++ app/helpers/issues_helper.rb | 16 ---------------- app/views/layouts/nav/_project.html.haml | 2 +- app/views/projects/issues/_head.html.haml | 2 +- 4 files changed, 9 insertions(+), 18 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 7f5c3ff3d6a..cb1e514c60e 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -5,6 +5,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleAwardEmoji include IssuableCollections + before_action :redirect_to_external_issue_tracker, only: [:index] before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, :related_branches, :can_create_branch] @@ -201,6 +202,12 @@ class Projects::IssuesController < Projects::ApplicationController return render_404 unless @project.issues_enabled && @project.default_issues_tracker? end + def redirect_to_external_issue_tracker + return unless @project.external_issue_tracker + + redirect_to @project.external_issue_tracker.issues_url + end + # Since iids are implemented only in 6.1 # user may navigate to issue page using old global ids. # diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 2b0defd1dda..5061ccb93a4 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -13,22 +13,6 @@ module IssuesHelper OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') end - def url_for_project_issues(project = @project, options = {}) - return '' if project.nil? - - url = - if options[:only_path] - project.issues_tracker.project_path - else - project.issues_tracker.project_url - end - - # Ensure we return a valid URL to prevent possible XSS. - URI.parse(url).to_s - rescue URI::InvalidURIError - '' - end - def url_for_new_issue(project = @project, options = {}) return '' if project.nil? diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 9e65d94186b..1d3b8fc3683 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -66,7 +66,7 @@ - if project_nav_tab? :issues = nav_link(controller: [:issues, :labels, :milestones]) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do + = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do %span Issues - if @project.default_issues_tracker? diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml index 403adb7426b..60b45115b73 100644 --- a/app/views/projects/issues/_head.html.haml +++ b/app/views/projects/issues/_head.html.haml @@ -2,7 +2,7 @@ %ul{ class: (container_class) } - if project_nav_tab?(:issues) && !current_controller?(:merge_requests) = nav_link(controller: :issues) do - = link_to url_for_project_issues(@project, only_path: true), title: 'Issues' do + = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do %span Issues -- cgit v1.2.3 From 901d4d2ca54d173f9c6b1f39c7548ef7fc9e8cd7 Mon Sep 17 00:00:00 2001 From: Robert Speicher Date: Mon, 1 Aug 2016 18:23:12 -0700 Subject: Remove `url_for_new_issue` helper Now we link to the standard `IssuesController#new` action, and let it redirect if we're using an external tracker. --- app/controllers/projects/issues_controller.rb | 12 +++++++++--- app/helpers/issues_helper.rb | 16 ---------------- app/views/projects/buttons/_dropdown.html.haml | 2 +- 3 files changed, 10 insertions(+), 20 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index cb1e514c60e..660e0eba06f 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -5,7 +5,7 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleAwardEmoji include IssuableCollections - before_action :redirect_to_external_issue_tracker, only: [:index] + before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :module_enabled before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, :related_branches, :can_create_branch] @@ -203,9 +203,15 @@ class Projects::IssuesController < Projects::ApplicationController end def redirect_to_external_issue_tracker - return unless @project.external_issue_tracker + external = @project.external_issue_tracker - redirect_to @project.external_issue_tracker.issues_url + return unless external + + if action_name == 'new' + redirect_to external.new_issue_path + else + redirect_to external.issues_url + end end # Since iids are implemented only in 6.1 diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 5061ccb93a4..2e82b44437b 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -13,22 +13,6 @@ module IssuesHelper OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned') end - def url_for_new_issue(project = @project, options = {}) - return '' if project.nil? - - url = - if options[:only_path] - project.issues_tracker.new_issue_path - else - project.issues_tracker.new_issue_url - end - - # Ensure we return a valid URL to prevent possible XSS. - URI.parse(url).to_s - rescue URI::InvalidURIError - '' - end - def url_for_issue(issue_iid, project = @project, options = {}) return '' if project.nil? diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 16b8e1cca91..ca907077c2b 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -9,7 +9,7 @@ - if can_create_issue %li - = link_to url_for_new_issue(@project, only_path: true) do + = link_to new_namespace_project_issue_path(@project.namespace, @project) do = icon('exclamation-circle fw') New issue -- cgit v1.2.3 From 010477edc034330dfe4bce7b4dfac252e1fb0a25 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 7 Jul 2016 12:41:57 +0100 Subject: Append .json onto graph request URL (!5136) --- app/views/projects/graphs/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index a985b442b2d..8777e0d8fcd 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -32,7 +32,7 @@ :javascript $.ajax({ type: "GET", - url: location.href, + url: "#{namespace_project_graph_path(@project.namespace, @project, current_ref, :json)}", dataType: "json", success: function (data) { var graph = new ContributorsStatGraph(); -- cgit v1.2.3 From e1832914df2eccea1730586b26e759b562e8b7c1 Mon Sep 17 00:00:00 2001 From: winniehell Date: Sun, 31 Jul 2016 22:52:44 +0200 Subject: Allow branch names ending with .json for graph and network page (!5579) --- app/views/projects/graphs/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml index 8777e0d8fcd..ac5f792d140 100644 --- a/app/views/projects/graphs/show.html.haml +++ b/app/views/projects/graphs/show.html.haml @@ -32,7 +32,7 @@ :javascript $.ajax({ type: "GET", - url: "#{namespace_project_graph_path(@project.namespace, @project, current_ref, :json)}", + url: "#{namespace_project_graph_path(@project.namespace, @project, current_ref, format: :json)}", dataType: "json", success: function (data) { var graph = new ContributorsStatGraph(); -- cgit v1.2.3 From f15fb92209ccea3e13916d2f2d3899008f9df578 Mon Sep 17 00:00:00 2001 From: winniehell Date: Tue, 2 Aug 2016 04:50:47 +0200 Subject: Revert "md5 and utf_encode js libraries" This reverts commit 1bba46d66117b4a96d279fd964a45fe673db658c. --- app/assets/javascripts/lib/utils/md5.js | 211 ------------------------ app/assets/javascripts/lib/utils/utf8_encode.js | 70 -------- 2 files changed, 281 deletions(-) delete mode 100644 app/assets/javascripts/lib/utils/md5.js delete mode 100644 app/assets/javascripts/lib/utils/utf8_encode.js (limited to 'app') diff --git a/app/assets/javascripts/lib/utils/md5.js b/app/assets/javascripts/lib/utils/md5.js deleted file mode 100644 index b63716eaad2..00000000000 --- a/app/assets/javascripts/lib/utils/md5.js +++ /dev/null @@ -1,211 +0,0 @@ -function md5 (str) { - // http://kevin.vanzonneveld.net - // + original by: Webtoolkit.info (http://www.webtoolkit.info/) - // + namespaced by: Michael White (http://getsprink.com) - // + tweaked by: Jack - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + input by: Brett Zamir (http://brett-zamir.me) - // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // - depends on: utf8_encode - // * example 1: md5('Kevin van Zonneveld'); - // * returns 1: '6e658d4bfcb59cc13f96c14450ac40b9' - var xl; - - var rotateLeft = function (lValue, iShiftBits) { - return (lValue << iShiftBits) | (lValue >>> (32 - iShiftBits)); - }; - - var addUnsigned = function (lX, lY) { - var lX4, lY4, lX8, lY8, lResult; - lX8 = (lX & 0x80000000); - lY8 = (lY & 0x80000000); - lX4 = (lX & 0x40000000); - lY4 = (lY & 0x40000000); - lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF); - if (lX4 & lY4) { - return (lResult ^ 0x80000000 ^ lX8 ^ lY8); - } - if (lX4 | lY4) { - if (lResult & 0x40000000) { - return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); - } else { - return (lResult ^ 0x40000000 ^ lX8 ^ lY8); - } - } else { - return (lResult ^ lX8 ^ lY8); - } - }; - - var _F = function (x, y, z) { - return (x & y) | ((~x) & z); - }; - var _G = function (x, y, z) { - return (x & z) | (y & (~z)); - }; - var _H = function (x, y, z) { - return (x ^ y ^ z); - }; - var _I = function (x, y, z) { - return (y ^ (x | (~z))); - }; - - var _FF = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_F(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _GG = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_G(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _HH = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_H(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var _II = function (a, b, c, d, x, s, ac) { - a = addUnsigned(a, addUnsigned(addUnsigned(_I(b, c, d), x), ac)); - return addUnsigned(rotateLeft(a, s), b); - }; - - var convertToWordArray = function (str) { - var lWordCount; - var lMessageLength = str.length; - var lNumberOfWords_temp1 = lMessageLength + 8; - var lNumberOfWords_temp2 = (lNumberOfWords_temp1 - (lNumberOfWords_temp1 % 64)) / 64; - var lNumberOfWords = (lNumberOfWords_temp2 + 1) * 16; - var lWordArray = new Array(lNumberOfWords - 1); - var lBytePosition = 0; - var lByteCount = 0; - while (lByteCount < lMessageLength) { - lWordCount = (lByteCount - (lByteCount % 4)) / 4; - lBytePosition = (lByteCount % 4) * 8; - lWordArray[lWordCount] = (lWordArray[lWordCount] | (str.charCodeAt(lByteCount) << lBytePosition)); - lByteCount++; - } - lWordCount = (lByteCount - (lByteCount % 4)) / 4; - lBytePosition = (lByteCount % 4) * 8; - lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80 << lBytePosition); - lWordArray[lNumberOfWords - 2] = lMessageLength << 3; - lWordArray[lNumberOfWords - 1] = lMessageLength >>> 29; - return lWordArray; - }; - - var wordToHex = function (lValue) { - var wordToHexValue = "", - wordToHexValue_temp = "", - lByte, lCount; - for (lCount = 0; lCount <= 3; lCount++) { - lByte = (lValue >>> (lCount * 8)) & 255; - wordToHexValue_temp = "0" + lByte.toString(16); - wordToHexValue = wordToHexValue + wordToHexValue_temp.substr(wordToHexValue_temp.length - 2, 2); - } - return wordToHexValue; - }; - - var x = [], - k, AA, BB, CC, DD, a, b, c, d, S11 = 7, - S12 = 12, - S13 = 17, - S14 = 22, - S21 = 5, - S22 = 9, - S23 = 14, - S24 = 20, - S31 = 4, - S32 = 11, - S33 = 16, - S34 = 23, - S41 = 6, - S42 = 10, - S43 = 15, - S44 = 21; - - str = this.utf8_encode(str); - x = convertToWordArray(str); - a = 0x67452301; - b = 0xEFCDAB89; - c = 0x98BADCFE; - d = 0x10325476; - - xl = x.length; - for (k = 0; k < xl; k += 16) { - AA = a; - BB = b; - CC = c; - DD = d; - a = _FF(a, b, c, d, x[k + 0], S11, 0xD76AA478); - d = _FF(d, a, b, c, x[k + 1], S12, 0xE8C7B756); - c = _FF(c, d, a, b, x[k + 2], S13, 0x242070DB); - b = _FF(b, c, d, a, x[k + 3], S14, 0xC1BDCEEE); - a = _FF(a, b, c, d, x[k + 4], S11, 0xF57C0FAF); - d = _FF(d, a, b, c, x[k + 5], S12, 0x4787C62A); - c = _FF(c, d, a, b, x[k + 6], S13, 0xA8304613); - b = _FF(b, c, d, a, x[k + 7], S14, 0xFD469501); - a = _FF(a, b, c, d, x[k + 8], S11, 0x698098D8); - d = _FF(d, a, b, c, x[k + 9], S12, 0x8B44F7AF); - c = _FF(c, d, a, b, x[k + 10], S13, 0xFFFF5BB1); - b = _FF(b, c, d, a, x[k + 11], S14, 0x895CD7BE); - a = _FF(a, b, c, d, x[k + 12], S11, 0x6B901122); - d = _FF(d, a, b, c, x[k + 13], S12, 0xFD987193); - c = _FF(c, d, a, b, x[k + 14], S13, 0xA679438E); - b = _FF(b, c, d, a, x[k + 15], S14, 0x49B40821); - a = _GG(a, b, c, d, x[k + 1], S21, 0xF61E2562); - d = _GG(d, a, b, c, x[k + 6], S22, 0xC040B340); - c = _GG(c, d, a, b, x[k + 11], S23, 0x265E5A51); - b = _GG(b, c, d, a, x[k + 0], S24, 0xE9B6C7AA); - a = _GG(a, b, c, d, x[k + 5], S21, 0xD62F105D); - d = _GG(d, a, b, c, x[k + 10], S22, 0x2441453); - c = _GG(c, d, a, b, x[k + 15], S23, 0xD8A1E681); - b = _GG(b, c, d, a, x[k + 4], S24, 0xE7D3FBC8); - a = _GG(a, b, c, d, x[k + 9], S21, 0x21E1CDE6); - d = _GG(d, a, b, c, x[k + 14], S22, 0xC33707D6); - c = _GG(c, d, a, b, x[k + 3], S23, 0xF4D50D87); - b = _GG(b, c, d, a, x[k + 8], S24, 0x455A14ED); - a = _GG(a, b, c, d, x[k + 13], S21, 0xA9E3E905); - d = _GG(d, a, b, c, x[k + 2], S22, 0xFCEFA3F8); - c = _GG(c, d, a, b, x[k + 7], S23, 0x676F02D9); - b = _GG(b, c, d, a, x[k + 12], S24, 0x8D2A4C8A); - a = _HH(a, b, c, d, x[k + 5], S31, 0xFFFA3942); - d = _HH(d, a, b, c, x[k + 8], S32, 0x8771F681); - c = _HH(c, d, a, b, x[k + 11], S33, 0x6D9D6122); - b = _HH(b, c, d, a, x[k + 14], S34, 0xFDE5380C); - a = _HH(a, b, c, d, x[k + 1], S31, 0xA4BEEA44); - d = _HH(d, a, b, c, x[k + 4], S32, 0x4BDECFA9); - c = _HH(c, d, a, b, x[k + 7], S33, 0xF6BB4B60); - b = _HH(b, c, d, a, x[k + 10], S34, 0xBEBFBC70); - a = _HH(a, b, c, d, x[k + 13], S31, 0x289B7EC6); - d = _HH(d, a, b, c, x[k + 0], S32, 0xEAA127FA); - c = _HH(c, d, a, b, x[k + 3], S33, 0xD4EF3085); - b = _HH(b, c, d, a, x[k + 6], S34, 0x4881D05); - a = _HH(a, b, c, d, x[k + 9], S31, 0xD9D4D039); - d = _HH(d, a, b, c, x[k + 12], S32, 0xE6DB99E5); - c = _HH(c, d, a, b, x[k + 15], S33, 0x1FA27CF8); - b = _HH(b, c, d, a, x[k + 2], S34, 0xC4AC5665); - a = _II(a, b, c, d, x[k + 0], S41, 0xF4292244); - d = _II(d, a, b, c, x[k + 7], S42, 0x432AFF97); - c = _II(c, d, a, b, x[k + 14], S43, 0xAB9423A7); - b = _II(b, c, d, a, x[k + 5], S44, 0xFC93A039); - a = _II(a, b, c, d, x[k + 12], S41, 0x655B59C3); - d = _II(d, a, b, c, x[k + 3], S42, 0x8F0CCC92); - c = _II(c, d, a, b, x[k + 10], S43, 0xFFEFF47D); - b = _II(b, c, d, a, x[k + 1], S44, 0x85845DD1); - a = _II(a, b, c, d, x[k + 8], S41, 0x6FA87E4F); - d = _II(d, a, b, c, x[k + 15], S42, 0xFE2CE6E0); - c = _II(c, d, a, b, x[k + 6], S43, 0xA3014314); - b = _II(b, c, d, a, x[k + 13], S44, 0x4E0811A1); - a = _II(a, b, c, d, x[k + 4], S41, 0xF7537E82); - d = _II(d, a, b, c, x[k + 11], S42, 0xBD3AF235); - c = _II(c, d, a, b, x[k + 2], S43, 0x2AD7D2BB); - b = _II(b, c, d, a, x[k + 9], S44, 0xEB86D391); - a = addUnsigned(a, AA); - b = addUnsigned(b, BB); - c = addUnsigned(c, CC); - d = addUnsigned(d, DD); - } - - var temp = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d); - - return temp.toLowerCase(); -} diff --git a/app/assets/javascripts/lib/utils/utf8_encode.js b/app/assets/javascripts/lib/utils/utf8_encode.js deleted file mode 100644 index 39ffe44dae0..00000000000 --- a/app/assets/javascripts/lib/utils/utf8_encode.js +++ /dev/null @@ -1,70 +0,0 @@ -function utf8_encode (argString) { - // http://kevin.vanzonneveld.net - // + original by: Webtoolkit.info (http://www.webtoolkit.info/) - // + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // + improved by: sowberry - // + tweaked by: Jack - // + bugfixed by: Onno Marsman - // + improved by: Yves Sucaet - // + bugfixed by: Onno Marsman - // + bugfixed by: Ulrich - // + bugfixed by: Rafal Kukawski - // + improved by: kirilloid - // + bugfixed by: kirilloid - // * example 1: utf8_encode('Kevin van Zonneveld'); - // * returns 1: 'Kevin van Zonneveld' - - if (argString === null || typeof argString === "undefined") { - return ""; - } - - var string = (argString + ''); // .replace(/\r\n/g, "\n").replace(/\r/g, "\n"); - var utftext = '', - start, end, stringl = 0; - - start = end = 0; - stringl = string.length; - for (var n = 0; n < stringl; n++) { - var c1 = string.charCodeAt(n); - var enc = null; - - if (c1 < 128) { - end++; - } else if (c1 > 127 && c1 < 2048) { - enc = String.fromCharCode( - (c1 >> 6) | 192, - ( c1 & 63) | 128 - ); - } else if (c1 & 0xF800 != 0xD800) { - enc = String.fromCharCode( - (c1 >> 12) | 224, - ((c1 >> 6) & 63) | 128, - ( c1 & 63) | 128 - ); - } else { // surrogate pairs - if (c1 & 0xFC00 != 0xD800) { throw new RangeError("Unmatched trail surrogate at " + n); } - var c2 = string.charCodeAt(++n); - if (c2 & 0xFC00 != 0xDC00) { throw new RangeError("Unmatched lead surrogate at " + (n-1)); } - c1 = ((c1 & 0x3FF) << 10) + (c2 & 0x3FF) + 0x10000; - enc = String.fromCharCode( - (c1 >> 18) | 240, - ((c1 >> 12) & 63) | 128, - ((c1 >> 6) & 63) | 128, - ( c1 & 63) | 128 - ); - } - if (enc !== null) { - if (end > start) { - utftext += string.slice(start, end); - } - utftext += enc; - start = end = n + 1; - } - } - - if (end > start) { - utftext += string.slice(start, stringl); - } - - return utftext; -} -- cgit v1.2.3 From edc5f4018e45327421e112de18d53bfbdabd38f9 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Mon, 1 Aug 2016 10:06:57 +0100 Subject: developer cannot push to protected branch when project is empty or he has not been granted permission to do so --- app/models/project.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) (limited to 'app') diff --git a/app/models/project.rb b/app/models/project.rb index 83b848ded8b..507813bccf8 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -870,10 +870,22 @@ class Project < ActiveRecord::Base # Check if current branch name is marked as protected in the system def protected_branch?(branch_name) + return true if empty_repo? && default_branch_protected? + @protected_branches ||= self.protected_branches.to_a ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? end + def developers_can_push_to_protected_branch?(branch_name) + return true if empty_repo? && !default_branch_protected? + + protected_branches.matching(branch_name).any?(&:developers_can_push) + end + + def developers_can_merge_to_protected_branch?(branch_name) + protected_branches.matching(branch_name).any?(&:developers_can_merge) + end + def forked? !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) end @@ -1265,6 +1277,10 @@ class Project < ActiveRecord::Base private + def default_branch_protected? + current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL + end + def authorized_for_user_by_group?(user, min_access_level) member = user.group_members.find_by(source_id: group) -- cgit v1.2.3 From 4768afbdbf85abbb5e2281c8855e7d27c07a581e Mon Sep 17 00:00:00 2001 From: Keith Pope Date: Tue, 2 Aug 2016 06:56:23 +0100 Subject: Add simple identifier to public SSH keys --- app/models/key.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/key.rb b/app/models/key.rb index b9bc38a0436..568a60b8af3 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -26,8 +26,9 @@ class Key < ActiveRecord::Base end def publishable_key - # Removes anything beyond the keytype and key itself - self.key.split[0..1].join(' ') + # Strip out the keys comment so we don't leak email addresses + # Replace with simple ident of user_name (hostname) + self.key.split[0..1].push("#{self.user_name} (#{Gitlab.config.gitlab.host})").join(' ') end # projects that has this key -- cgit v1.2.3 From f0b73f81198a9cba8961774f45e0b96f0cb73c78 Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 31 May 2016 11:06:45 +0530 Subject: Add help document describing wiki linking behavior. --- app/views/projects/wikis/_form.html.haml | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app') diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 797a1a59e9f..893bea3c759 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -20,6 +20,8 @@ .help-block To link to a (new) page, simply type %code [Link Title](page-slug) + \. More examples are in the + = link_to 'documentation', help_page_path(category: 'markdown', file: 'wiki', format: 'md') \. .form-group = f.label :commit_message, class: 'control-label' -- cgit v1.2.3 From d55e83addecda6c6a3632826b9bb61437d18290f Mon Sep 17 00:00:00 2001 From: Timothy Andrew Date: Tue, 31 May 2016 15:59:16 +0530 Subject: Use `succeed` to add periods to help text. --- app/views/projects/wikis/_form.html.haml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 893bea3c759..6360371804a 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -18,11 +18,14 @@ .error-alert .help-block - To link to a (new) page, simply type - %code [Link Title](page-slug) - \. More examples are in the - = link_to 'documentation', help_page_path(category: 'markdown', file: 'wiki', format: 'md') - \. + = succeed '.' do + To link to a (new) page, simply type + %code [Link Title](page-slug) + + = succeed '.' do + More examples are in the + = link_to 'documentation', help_page_path(category: 'markdown', file: 'wiki', format: 'md') + .form-group = f.label :commit_message, class: 'control-label' .col-sm-10= f.text_field :message, class: 'form-control', rows: 18 -- cgit v1.2.3 From 020ea32e767b9ad033f9fedcaa902865a01fa944 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 2 Aug 2016 18:06:31 +0800 Subject: Implement pipeline hooks, extracted from !5525 Closes #20115 --- app/controllers/concerns/service_params.rb | 15 +++++++-------- app/controllers/projects/hooks_controller.rb | 1 + app/models/ci/pipeline.rb | 10 +++++++++- app/models/hooks/project_hook.rb | 1 + app/models/hooks/web_hook.rb | 1 + app/models/service.rb | 5 +++++ app/services/ci/create_pipeline_service.rb | 1 + app/views/projects/hooks/_project_hook.html.haml | 2 +- app/views/shared/web_hooks/_form.html.haml | 7 +++++++ 9 files changed, 33 insertions(+), 10 deletions(-) (limited to 'app') diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index 471d15af913..58877c5ad5d 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -7,11 +7,12 @@ module ServiceParams :build_key, :server, :teamcity_url, :drone_url, :build_type, :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :colorize_messages, :channels, - :push_events, :issues_events, :merge_requests_events, :tag_push_events, - :note_events, :build_events, :wiki_page_events, - :notify_only_broken_builds, :add_pusher, - :send_from_committer_email, :disable_diffs, :external_wiki_url, - :notify, :color, + # See app/helpers/services_helper.rb + # for why we need issues_events and merge_requests_events. + :issues_events, :merge_requests_events, + :notify_only_broken_builds, :notify_only_broken_pipelines, + :add_pusher, :send_from_committer_email, :disable_diffs, + :external_wiki_url, :notify, :color, :server_host, :server_port, :default_irc_uri, :enable_ssl_verification, :jira_issue_transition_id] @@ -19,9 +20,7 @@ module ServiceParams FILTER_BLANK_PARAMS = [:password] def service_params - dynamic_params = [] - dynamic_params.concat(@service.event_channel_names) - + dynamic_params = @service.event_channel_names + @service.event_names service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params) if service_params[:service].is_a?(Hash) diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index a60027ff477..b5624046387 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -56,6 +56,7 @@ class Projects::HooksController < Projects::ApplicationController def hook_params params.require(:hook).permit( :build_events, + :pipeline_events, :enable_ssl_verification, :issues_events, :merge_requests_events, diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bce6a992af6..4e6ccf48c68 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -237,7 +237,15 @@ module Ci self.started_at = statuses.started_at self.finished_at = statuses.finished_at self.duration = statuses.latest.duration - save + saved = save + execute_hooks if saved && !skip_ci? + saved + end + + def execute_hooks + pipeline_data = Gitlab::DataBuilder::PipelineDataBuilder.build(self) + project.execute_hooks(pipeline_data, :pipeline_hooks) + project.execute_services(pipeline_data.dup, :pipeline_hooks) end def keep_around_commits diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index ba42a8eeb70..836a75b0608 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -5,5 +5,6 @@ class ProjectHook < WebHook scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } scope :build_hooks, -> { where(build_events: true) } + scope :pipeline_hooks, -> { where(pipeline_events: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true) } end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 8b87b6c3d64..f365dee3141 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -8,6 +8,7 @@ class WebHook < ActiveRecord::Base default_value_for :merge_requests_events, false default_value_for :tag_push_events, false default_value_for :build_events, false + default_value_for :pipeline_events, false default_value_for :enable_ssl_verification, true scope :push_hooks, -> { where(push_events: true) } diff --git a/app/models/service.rb b/app/models/service.rb index 40cd9b861f0..e4cd44f542a 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -36,6 +36,7 @@ class Service < ActiveRecord::Base scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) } scope :build_hooks, -> { where(build_events: true, active: true) } + scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } @@ -86,6 +87,10 @@ class Service < ActiveRecord::Base [] end + def event_names + supported_events.map { |event| "#{event}_events" } + end + def event_field(event) nil end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index be91bf0db85..7a8b0683acb 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,6 +27,7 @@ module Ci end pipeline.save! + pipeline.touch unless pipeline.create_builds(current_user) pipeline.errors.add(:base, 'No builds for this pipeline.') diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml index 8151187d499..3fcf1692e09 100644 --- a/app/views/projects/hooks/_project_hook.html.haml +++ b/app/views/projects/hooks/_project_hook.html.haml @@ -3,7 +3,7 @@ .col-md-8.col-lg-7 %strong.light-header= hook.url %div - - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events wiki_page_events).each do |trigger| + - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray.deploy-project-label= trigger.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 2585ed9360b..106161d6515 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -65,6 +65,13 @@ %strong Build events %p.light This url will be triggered when the build status changes + %li + = f.check_box :pipeline_events, class: 'pull-left' + .prepend-left-20 + = f.label :pipeline_events, class: 'list-label' do + %strong Pipeline events + %p.light + This url will be triggered when the pipeline status changes %li = f.check_box :wiki_page_events, class: 'pull-left' .prepend-left-20 -- cgit v1.2.3 From fc492c890596c996ad270dcbcd5d76c2b748cbeb Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Tue, 2 Aug 2016 13:42:14 +0300 Subject: Change Markdown document location --- app/views/projects/wikis/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 6360371804a..643f7c589e6 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -24,7 +24,7 @@ = succeed '.' do More examples are in the - = link_to 'documentation', help_page_path(category: 'markdown', file: 'wiki', format: 'md') + = link_to 'documentation', help_page_path("user/project/markdown", anchor: "wiki-specific-markdown") .form-group = f.label :commit_message, class: 'control-label' -- cgit v1.2.3 From a33a2ba0a60930cf617675186f7c59139232b44c Mon Sep 17 00:00:00 2001 From: Elias Werberich Date: Sun, 31 Jul 2016 17:34:27 +0200 Subject: Fix confusing description of a blocked user. --- app/views/admin/users/show.html.haml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index d37489bebea..76c9ed0ee8b 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -140,12 +140,10 @@ .panel-heading This user is blocked .panel-body - %p Blocking user has the following effects: + %p A blocked user cannot: %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li Personal projects will be left - %li Owned groups will be left + %li Log in + %li Access Git repositories %br = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } - else -- cgit v1.2.3 From 8716ff7f63fff0b056e110bef930c32a98e86c63 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Mon, 1 Aug 2016 16:55:51 +0200 Subject: Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible - Preloading noteable we share the same noteable instance when more than one discussion refers to the same noteable. - Any other call to that object that is cached in that object will be for any discussion. - In those cases where merge_request_diff has all the sha stored to build a diff_refs get that diff_refs using directly those sha instead accessing to the git repository to first get the commits and later the sha. --- app/controllers/projects/merge_requests_controller.rb | 2 ++ app/helpers/notes_helper.rb | 4 ++++ app/models/diff_note.rb | 12 ++++++++++-- app/models/discussion.rb | 6 ++++++ app/models/merge_request.rb | 15 ++++++++++++++- app/models/merge_request_diff.rb | 4 ++++ 6 files changed, 40 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 03166294ddd..116e7904a4e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -378,6 +378,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController fresh. discussions + preload_noteable_for_regular_notes(@discussions.flat_map(&:notes)) + # This is not executed lazily @notes = Banzai::NoteRenderer.render( @discussions.flat_map(&:notes), diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 0c47abe0fba..26bde2230a9 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -92,6 +92,10 @@ module NotesHelper project.team.max_member_access_for_user_ids(user_ids) end + def preload_noteable_for_regular_notes(notes) + ActiveRecord::Associations::Preloader.new.preload(notes.select { |note| !note.for_commit? }, :noteable) + end + def note_max_access_for_user(note) note.project.team.human_max_access(note.author_id) end diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 9671955db36..c816deb4e0c 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -67,7 +67,7 @@ class DiffNote < Note return false unless supported? return true if for_commit? - diff_refs ||= self.noteable.diff_refs + diff_refs ||= noteable_diff_refs self.position.diff_refs == diff_refs end @@ -78,6 +78,14 @@ class DiffNote < Note !self.for_merge_request? || self.noteable.support_new_diff_notes? end + def noteable_diff_refs + if noteable.respond_to?(:diff_sha_refs) + noteable.diff_sha_refs + else + noteable.diff_refs + end + end + def set_original_position self.original_position = self.position.dup end @@ -96,7 +104,7 @@ class DiffNote < Note self.project, nil, old_diff_refs: self.position.diff_refs, - new_diff_refs: self.noteable.diff_refs, + new_diff_refs: noteable_diff_refs, paths: self.position.paths ).execute(self) end diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 74facfd1c9c..e2218a5f02b 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -49,6 +49,12 @@ class Discussion self.noteable == target && !diff_discussion? end + def active? + return @active if defined?(@active) + + @active = first_note.active? + end + def expanded? !diff_discussion? || active? end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f1b9c1d6feb..a99c4ba52a4 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -255,6 +255,19 @@ class MergeRequest < ActiveRecord::Base ) end + # Return diff_refs instance trying to not touch the git repository + def diff_sha_refs + if merge_request_diff && merge_request_diff.diff_refs_by_sha? + return Gitlab::Diff::DiffRefs.new( + base_sha: merge_request_diff.base_commit_sha, + start_sha: merge_request_diff.start_commit_sha, + head_sha: merge_request_diff.head_commit_sha + ) + else + diff_refs + end + end + def validate_branches if target_project == source_project && target_branch == source_branch errors.add :branch_conflict, "You can not use same project/branch for source and target" @@ -659,7 +672,7 @@ class MergeRequest < ActiveRecord::Base end def support_new_diff_notes? - diff_refs && diff_refs.complete? + diff_sha_refs && diff_sha_refs.complete? end def update_diff_notes_positions(old_diff_refs:, new_diff_refs:) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 3f520c8f3ff..119266f2d2c 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -82,6 +82,10 @@ class MergeRequestDiff < ActiveRecord::Base project.commit(self.head_commit_sha) end + def diff_refs_by_sha? + base_commit_sha? && head_commit_sha? && start_commit_sha? + end + def compare @compare ||= begin -- cgit v1.2.3 From e4c517a635b6a45a9afc65b37682cc4b4951e922 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 25 Jul 2016 23:49:06 -0500 Subject: Expand commit message width in repo view --- app/assets/stylesheets/pages/tree.scss | 4 ++++ app/models/commit.rb | 14 ++++++++------ app/views/projects/tree/_tree_commit_column.html.haml | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 390977297fb..9da40fe2b09 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -58,6 +58,10 @@ .tree_commit { max-width: 320px; + + .str-truncated { + max-width: 100%; + } } .tree_time_ago { diff --git a/app/models/commit.rb b/app/models/commit.rb index 486ad6714d9..c52b4a051c2 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -123,15 +123,17 @@ class Commit # In case this first line is longer than 100 characters, it is cut off # after 80 characters and ellipses (`&hellp;`) are appended. def title - title = safe_message + full_title.length > 100 ? full_title[0..79] << "…" : full_title + end - return no_commit_message if title.blank? + # Returns the full commits title + def full_title + return @full_title if @full_title - title_end = title.index("\n") - if (!title_end && title.length > 100) || (title_end && title_end > 100) - title[0..79] << "…" + if safe_message.blank? + @full_title = no_commit_message else - title.split("\n", 2).first + @full_title = safe_message.split("\n", 2).first end end diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index a3a4bd4f752..84da16b6bb1 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,2 +1,2 @@ %span.str-truncated - = link_to_gfm commit.title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link" + = link_to_gfm commit.full_title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link" -- cgit v1.2.3 From 51bcdfb7850a642c1062d5ab73417a6c6d2edb51 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 2 Aug 2016 18:01:14 -0500 Subject: Fix filter input alignment --- app/assets/stylesheets/framework/nav.scss | 2 -- 1 file changed, 2 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 364952d3b4a..7852fc9a424 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -182,7 +182,6 @@ > form { display: inline-block; - margin-top: -1px; } .icon-label { @@ -193,7 +192,6 @@ height: 35px; display: inline-block; position: relative; - top: 2px; margin-right: $gl-padding-top; /* Medium devices (desktops, 992px and up) */ -- cgit v1.2.3 From cd7c2cb6ddd4d9c9f9bdae00c887c0022c121c17 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 20 Jul 2016 18:25:36 +0200 Subject: Cache highlighted diff lines for merge requests Introducing the concept of SafeDiffs which relates diffs with UI highlighting. --- app/controllers/concerns/diff_for_path.rb | 6 +-- app/controllers/projects/commit_controller.rb | 4 +- app/controllers/projects/compare_controller.rb | 6 +-- .../projects/merge_requests_controller.rb | 10 ++-- app/helpers/commits_helper.rb | 4 +- app/helpers/diff_helper.rb | 11 ++--- app/models/merge_request.rb | 2 + app/models/safe_diffs.rb | 5 ++ app/models/safe_diffs/base.rb | 55 ++++++++++++++++++++++ app/models/safe_diffs/commit.rb | 10 ++++ app/models/safe_diffs/compare.rb | 10 ++++ app/models/safe_diffs/merge_request.rb | 52 ++++++++++++++++++++ .../merge_request_diff_cache_service.rb | 8 ++++ app/views/notify/repository_push_email.html.haml | 2 +- app/views/projects/commit/show.html.haml | 2 +- app/views/projects/compare/show.html.haml | 2 +- app/views/projects/diffs/_diffs.html.haml | 2 - app/views/projects/diffs/_file.html.haml | 4 +- app/views/projects/diffs/_text_file.html.haml | 2 +- .../projects/merge_requests/_new_submit.html.haml | 2 +- .../projects/merge_requests/show/_diffs.html.haml | 5 +- 21 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 app/models/safe_diffs.rb create mode 100644 app/models/safe_diffs/base.rb create mode 100644 app/models/safe_diffs/commit.rb create mode 100644 app/models/safe_diffs/compare.rb create mode 100644 app/models/safe_diffs/merge_request.rb create mode 100644 app/services/merge_requests/merge_request_diff_cache_service.rb (limited to 'app') diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb index 026d8b2e1e0..aeec3009f15 100644 --- a/app/controllers/concerns/diff_for_path.rb +++ b/app/controllers/concerns/diff_for_path.rb @@ -1,8 +1,8 @@ module DiffForPath extend ActiveSupport::Concern - def render_diff_for_path(diffs, diff_refs, project) - diff_file = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository).find do |diff| + def render_diff_for_path(diffs) + diff_file = diffs.diff_files.find do |diff| diff.old_path == params[:old_path] && diff.new_path == params[:new_path] end @@ -14,7 +14,7 @@ module DiffForPath locals = { diff_file: diff_file, diff_commit: diff_commit, - diff_refs: diff_refs, + diff_refs: diffs.diff_refs, blob: blob, project: project } diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 7ae034f9398..6060b6e55bc 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -28,7 +28,7 @@ class Projects::CommitController < Projects::ApplicationController end def diff_for_path - render_diff_for_path(@diffs, @commit.diff_refs, @project) + render_diff_for_path(SafeDiffs::Commit.new(@commit, diff_options: diff_options)) end def builds @@ -110,7 +110,7 @@ class Projects::CommitController < Projects::ApplicationController opts = diff_options opts[:ignore_whitespace_change] = true if params[:format] == 'diff' - @diffs = commit.diffs(opts) + @diffs = SafeDiffs::Commit.new(commit, diff_options: opts) @notes_count = commit.notes.count end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 8c004724f02..2eda950a1bd 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(@diffs, @diff_refs, @project) + render_diff_for_path(SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options)) end def create @@ -46,12 +46,12 @@ class Projects::CompareController < Projects::ApplicationController @commit = @project.commit(@head_ref) @base_commit = @project.merge_base_commit(@start_ref, @head_ref) - @diffs = @compare.diffs(diff_options) - @diff_refs = Gitlab::Diff::DiffRefs.new( + diff_refs = Gitlab::Diff::DiffRefs.new( base_sha: @base_commit.try(:sha), start_sha: @start_commit.try(:sha), head_sha: @commit.try(:sha) ) + @diffs = SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options, diff_refs: diff_refs) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 116e7904a4e..78a6a3c5715 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -103,9 +103,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end define_commit_vars - diffs = @merge_request.diffs(diff_options) - render_diff_for_path(diffs, @merge_request.diff_refs, @merge_request.project) + render_diff_for_path(SafeDiffs::MergeRequest.new(merge_request, diff_options: diff_options)) end def commits @@ -153,7 +152,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.compare_commits.reverse @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit - @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare + if @merge_request.compare + @diffs = SafeDiffs::Compare.new(@merge_request.compare, + project: @merge_request.project, + diff_refs: @merge_request.diff_refs, + diff_options: diff_options) + end @diff_notes_disabled = true @pipeline = @merge_request.pipeline diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index f497626e21a..7a02d0b10d9 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -206,10 +206,10 @@ module CommitsHelper end end - def view_file_btn(commit_sha, diff, project) + def view_file_btn(commit_sha, diff_new_path, project) link_to( namespace_project_blob_path(project.namespace, project, - tree_join(commit_sha, diff.new_path)), + tree_join(commit_sha, diff_new_path)), class: 'btn view-file js-view-file btn-file-option' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f35e2f6ddcd..6497282af57 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -23,18 +23,17 @@ module DiffHelper end def diff_options - options = { ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? } + options = SafeDiffs.default_options.merge( + ignore_whitespace_change: hide_whitespace?, + no_collapse: expand_all_diffs? + ) if action_name == 'diff_for_path' options[:no_collapse] = true options[:paths] = params.values_at(:old_path, :new_path) end - Commit.max_diff_options.merge(options) - end - - def safe_diff_files(diffs, diff_refs: nil, repository: nil) - diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) } + options end def unfold_bottom_class(bottom) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a99c4ba52a4..774851cc90f 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -313,6 +313,8 @@ class MergeRequest < ActiveRecord::Base merge_request_diff.reload_content + MergeRequests::MergeRequestDiffCacheService.new.execute(self) + new_diff_refs = self.diff_refs update_diff_notes_positions( diff --git a/app/models/safe_diffs.rb b/app/models/safe_diffs.rb new file mode 100644 index 00000000000..8ca9ec4cc39 --- /dev/null +++ b/app/models/safe_diffs.rb @@ -0,0 +1,5 @@ +module SafeDiffs + def self.default_options + ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false) + end +end diff --git a/app/models/safe_diffs/base.rb b/app/models/safe_diffs/base.rb new file mode 100644 index 00000000000..dfc4708e293 --- /dev/null +++ b/app/models/safe_diffs/base.rb @@ -0,0 +1,55 @@ +module SafeDiffs + class Base + attr_reader :project, :diff_options, :diff_view, :diff_refs + + delegate :count, :real_size, to: :diff_files + + def initialize(diffs, project:, diff_options:, diff_refs: nil) + @diffs = diffs + @project = project + @diff_options = diff_options + @diff_refs = diff_refs + end + + def diff_files + @diff_files ||= begin + diffs = @diffs.decorate! do |diff| + Gitlab::Diff::File.new(diff, diff_refs: @diff_refs, repository: @project.repository) + end + + highlight!(diffs) + diffs + end + end + + private + + def highlight!(diff_files) + if cacheable? + cache_highlight!(diff_files) + else + diff_files.each { |diff_file| highlight_diff_file!(diff_file) } + end + end + + def cacheable? + false + end + + def cache_highlight! + raise NotImplementedError + end + + def highlight_diff_file_from_cache!(diff_file, cache_diff_lines) + diff_file.diff_lines = cache_diff_lines.map do |line| + Gitlab::Diff::Line.init_from_hash(line) + end + end + + def highlight_diff_file!(diff_file) + diff_file.diff_lines = Gitlab::Diff::Highlight.new(diff_file, repository: diff_file.repository).highlight + diff_file.highlighted_diff_lines = diff_file.diff_lines # To be used on parallel diff + diff_file + end + end +end diff --git a/app/models/safe_diffs/commit.rb b/app/models/safe_diffs/commit.rb new file mode 100644 index 00000000000..338878f32e0 --- /dev/null +++ b/app/models/safe_diffs/commit.rb @@ -0,0 +1,10 @@ +module SafeDiffs + class Commit < Base + def initialize(commit, diff_options:) + super(commit.diffs(diff_options), + project: commit.project, + diff_options: diff_options, + diff_refs: commit.diff_refs) + end + end +end diff --git a/app/models/safe_diffs/compare.rb b/app/models/safe_diffs/compare.rb new file mode 100644 index 00000000000..6b64b81137d --- /dev/null +++ b/app/models/safe_diffs/compare.rb @@ -0,0 +1,10 @@ +module SafeDiffs + class Compare < Base + def initialize(compare, project:, diff_options:, diff_refs: nil) + super(compare.diffs(diff_options), + project: project, + diff_options: diff_options, + diff_refs: diff_refs) + end + end +end diff --git a/app/models/safe_diffs/merge_request.rb b/app/models/safe_diffs/merge_request.rb new file mode 100644 index 00000000000..111b9a54f91 --- /dev/null +++ b/app/models/safe_diffs/merge_request.rb @@ -0,0 +1,52 @@ +module SafeDiffs + class MergeRequest < Base + def initialize(merge_request, diff_options:) + @merge_request = merge_request + + super(merge_request.diffs(diff_options), + project: merge_request.project, + diff_options: diff_options, + diff_refs: merge_request.diff_refs) + end + + private + + # + # If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted) + # for the highlighted ones, so we just skip their execution. + # If the highlighted diff files lines are not cached we calculate and cache them. + # + # The content of the cache is and Hash where the key correspond to the file_path and the values are Arrays of + # hashes than represent serialized diff lines. + # + def cache_highlight!(diff_files) + highlighted_cache = Rails.cache.read(cache_key) || {} + highlighted_cache_was_empty = highlighted_cache.empty? + + diff_files.each do |diff_file| + file_path = diff_file.file_path + + if highlighted_cache[file_path] + highlight_diff_file_from_cache!(diff_file, highlighted_cache[file_path]) + else + highlight_diff_file!(diff_file) + highlighted_cache[file_path] = diff_file.diff_lines.map(&:to_hash) + end + end + + if highlighted_cache_was_empty + Rails.cache.write(cache_key, highlighted_cache) + end + + diff_files + end + + def cacheable? + @merge_request.merge_request_diff.present? + end + + def cache_key + [@merge_request.merge_request_diff, 'highlighted-safe-diff-files', diff_options] + end + end +end diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb new file mode 100644 index 00000000000..0a1905f7137 --- /dev/null +++ b/app/services/merge_requests/merge_request_diff_cache_service.rb @@ -0,0 +1,8 @@ +module MergeRequests + class MergeRequestDiffCacheService + def execute(merge_request) + # Executing the iteration we cache all the highlighted diff information + SafeDiffs::MergeRequest.new(merge_request, diff_options: SafeDiffs.default_options).diff_files.to_a + end + end +end diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index c161ecc3463..2d1a98caeaa 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -75,7 +75,7 @@ - blob = diff_file.blob - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) %table.code.white - - diff_file.highlighted_diff_lines.each do |line| + - diff_file.diff_lines.each do |line| = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true - else No preview for this file type diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index d0da2606587..11b2020f99b 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -7,7 +7,7 @@ = render "ci_menu" - else %div.block-connector -= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @commit.diff_refs += render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 28a50e7031a..eb8a1bd5289 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -8,7 +8,7 @@ - if @commits.present? = render "projects/commits/commit_list" - = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs + = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs - else .light-well .center diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 4bf3ccace20..45895a9a3de 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -2,8 +2,6 @@ - if diff_view == 'parallel' - fluid_layout true -- diff_files = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository) - .content-block.oneline-block.files-changed .inline-parallel-buttons - if !expand_all_diffs? && diff_files.any? { |diff_file| diff_file.collapsed? } diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index 1854c64cbd7..f914e13a1ec 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -15,6 +15,6 @@ from_merge_request_id: @merge_request.id, skip_visible_check: true) - = view_file_btn(diff_commit.id, diff_file, project) + = view_file_btn(diff_commit.id, diff_file.new_path, project) - = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, diff_refs: diff_refs, blob: blob, project: project + = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 5970b9abf2b..a483927671e 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -5,7 +5,7 @@ %table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' } - last_line = 0 - - diff_file.highlighted_diff_lines.each do |line| + - diff_file.diff_lines.each do |line| - last_line = line.new_pos = render "projects/diffs/line", line: line, diff_file: diff_file diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index a5e67b95727..cb2b623691c 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -42,7 +42,7 @@ %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. %p To preserve performance the line changes are not shown. - else - = render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false + = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false - if @pipeline #builds.builds.tab-pane = render "projects/merge_requests/show/builds" diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 1b0bae86ad4..ed2765356db 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,6 +1,7 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diffs: @merge_request.diffs(diff_options), - project: @merge_request.project, diff_refs: @merge_request.diff_refs + - diffs = SafeDiffs::MergeRequest.new(@merge_request, diff_options: diff_options) + = render "projects/diffs/diffs", diff_files: diffs.diff_files, + diff_refs: diffs.diff_refs, project: diffs.project - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else -- cgit v1.2.3 From 8f359ea9170b984ad43d126e17628c31ac3a1f14 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Tue, 26 Jul 2016 09:21:42 +0200 Subject: Move to Gitlab::Diff::FileCollection Instead calling diff_collection.count use diff_collection.size which is cache on the diff_collection --- app/controllers/projects/commit_controller.rb | 4 +- app/controllers/projects/compare_controller.rb | 4 +- .../projects/merge_requests_controller.rb | 16 ++++--- app/helpers/diff_helper.rb | 2 +- app/models/commit.rb | 4 ++ app/models/compare.rb | 23 +++++++++ app/models/merge_request.rb | 4 ++ app/models/safe_diffs.rb | 5 -- app/models/safe_diffs/base.rb | 55 ---------------------- app/models/safe_diffs/commit.rb | 10 ---- app/models/safe_diffs/compare.rb | 10 ---- app/models/safe_diffs/merge_request.rb | 52 -------------------- .../merge_request_diff_cache_service.rb | 2 +- app/views/notify/repository_push_email.html.haml | 2 +- app/views/projects/commit/_ci_menu.html.haml | 2 +- app/views/projects/diffs/_text_file.html.haml | 2 +- .../projects/merge_requests/show/_diffs.html.haml | 5 +- app/workers/irker_worker.rb | 2 +- 18 files changed, 53 insertions(+), 151 deletions(-) create mode 100644 app/models/compare.rb delete mode 100644 app/models/safe_diffs.rb delete mode 100644 app/models/safe_diffs/base.rb delete mode 100644 app/models/safe_diffs/commit.rb delete mode 100644 app/models/safe_diffs/compare.rb delete mode 100644 app/models/safe_diffs/merge_request.rb (limited to 'app') diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 6060b6e55bc..771a86530cd 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -28,7 +28,7 @@ class Projects::CommitController < Projects::ApplicationController end def diff_for_path - render_diff_for_path(SafeDiffs::Commit.new(@commit, diff_options: diff_options)) + render_diff_for_path(@commit.diff_file_collection(diff_options)) end def builds @@ -110,7 +110,7 @@ class Projects::CommitController < Projects::ApplicationController opts = diff_options opts[:ignore_whitespace_change] = true if params[:format] == 'diff' - @diffs = SafeDiffs::Commit.new(commit, diff_options: opts) + @diffs = commit.diff_file_collection(opts) @notes_count = commit.notes.count end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 2eda950a1bd..252ddfa429a 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options)) + render_diff_for_path(Compare.decorate(@compare, @project).diff_file_collection(diff_options: diff_options)) end def create @@ -51,7 +51,7 @@ class Projects::CompareController < Projects::ApplicationController start_sha: @start_commit.try(:sha), head_sha: @commit.try(:sha) ) - @diffs = SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options, diff_refs: diff_refs) + @diffs = Compare.decorate(@compare, @project).diff_file_collection(diff_options: diff_options, diff_refs: diff_refs) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 78a6a3c5715..39e7d0f6182 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -85,7 +85,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html { define_discussion_vars } - format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } } + format.json do + @diffs = @merge_request.diff_file_collection(diff_options) if @merge_request_diff.collected? + + render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } + end end end @@ -104,7 +108,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController define_commit_vars - render_diff_for_path(SafeDiffs::MergeRequest.new(merge_request, diff_options: diff_options)) + render_diff_for_path(@merge_request.diff_file_collection(diff_options)) end def commits @@ -153,10 +157,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit if @merge_request.compare - @diffs = SafeDiffs::Compare.new(@merge_request.compare, - project: @merge_request.project, - diff_refs: @merge_request.diff_refs, - diff_options: diff_options) + @diffs = Compare.decorate(@merge_request.compare, @project).diff_file_collection( + diff_options: diff_options, + diff_refs: @merge_request.diff_refs + ) end @diff_notes_disabled = true diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 6497282af57..2abe24b78bf 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -23,7 +23,7 @@ module DiffHelper end def diff_options - options = SafeDiffs.default_options.merge( + options = Gitlab::Diff::FileCollection.default_options.merge( ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? ) diff --git a/app/models/commit.rb b/app/models/commit.rb index c52b4a051c2..d22ecb222e5 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -317,6 +317,10 @@ class Commit nil end + def diff_file_collection(diff_options) + Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) + end + private def find_author_by_any_email diff --git a/app/models/compare.rb b/app/models/compare.rb new file mode 100644 index 00000000000..6672d1bf059 --- /dev/null +++ b/app/models/compare.rb @@ -0,0 +1,23 @@ +class Compare + delegate :commits, :same, :head, :base, to: :@compare + + def self.decorate(compare, project) + if compare.is_a?(Compare) + compare + else + self.new(compare, project) + end + end + + def initialize(compare, project) + @compare = compare + @project = project + end + + def diff_file_collection(diff_options:, diff_refs: nil) + Gitlab::Diff::FileCollection::Compare.new(@compare, + project: @project, + diff_options: diff_options, + diff_refs: diff_refs) + end +end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 774851cc90f..abc8bacbe59 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -168,6 +168,10 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args) end + def diff_file_collection(diff_options) + Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) + end + def diff_size merge_request_diff.size end diff --git a/app/models/safe_diffs.rb b/app/models/safe_diffs.rb deleted file mode 100644 index 8ca9ec4cc39..00000000000 --- a/app/models/safe_diffs.rb +++ /dev/null @@ -1,5 +0,0 @@ -module SafeDiffs - def self.default_options - ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false) - end -end diff --git a/app/models/safe_diffs/base.rb b/app/models/safe_diffs/base.rb deleted file mode 100644 index dfc4708e293..00000000000 --- a/app/models/safe_diffs/base.rb +++ /dev/null @@ -1,55 +0,0 @@ -module SafeDiffs - class Base - attr_reader :project, :diff_options, :diff_view, :diff_refs - - delegate :count, :real_size, to: :diff_files - - def initialize(diffs, project:, diff_options:, diff_refs: nil) - @diffs = diffs - @project = project - @diff_options = diff_options - @diff_refs = diff_refs - end - - def diff_files - @diff_files ||= begin - diffs = @diffs.decorate! do |diff| - Gitlab::Diff::File.new(diff, diff_refs: @diff_refs, repository: @project.repository) - end - - highlight!(diffs) - diffs - end - end - - private - - def highlight!(diff_files) - if cacheable? - cache_highlight!(diff_files) - else - diff_files.each { |diff_file| highlight_diff_file!(diff_file) } - end - end - - def cacheable? - false - end - - def cache_highlight! - raise NotImplementedError - end - - def highlight_diff_file_from_cache!(diff_file, cache_diff_lines) - diff_file.diff_lines = cache_diff_lines.map do |line| - Gitlab::Diff::Line.init_from_hash(line) - end - end - - def highlight_diff_file!(diff_file) - diff_file.diff_lines = Gitlab::Diff::Highlight.new(diff_file, repository: diff_file.repository).highlight - diff_file.highlighted_diff_lines = diff_file.diff_lines # To be used on parallel diff - diff_file - end - end -end diff --git a/app/models/safe_diffs/commit.rb b/app/models/safe_diffs/commit.rb deleted file mode 100644 index 338878f32e0..00000000000 --- a/app/models/safe_diffs/commit.rb +++ /dev/null @@ -1,10 +0,0 @@ -module SafeDiffs - class Commit < Base - def initialize(commit, diff_options:) - super(commit.diffs(diff_options), - project: commit.project, - diff_options: diff_options, - diff_refs: commit.diff_refs) - end - end -end diff --git a/app/models/safe_diffs/compare.rb b/app/models/safe_diffs/compare.rb deleted file mode 100644 index 6b64b81137d..00000000000 --- a/app/models/safe_diffs/compare.rb +++ /dev/null @@ -1,10 +0,0 @@ -module SafeDiffs - class Compare < Base - def initialize(compare, project:, diff_options:, diff_refs: nil) - super(compare.diffs(diff_options), - project: project, - diff_options: diff_options, - diff_refs: diff_refs) - end - end -end diff --git a/app/models/safe_diffs/merge_request.rb b/app/models/safe_diffs/merge_request.rb deleted file mode 100644 index 111b9a54f91..00000000000 --- a/app/models/safe_diffs/merge_request.rb +++ /dev/null @@ -1,52 +0,0 @@ -module SafeDiffs - class MergeRequest < Base - def initialize(merge_request, diff_options:) - @merge_request = merge_request - - super(merge_request.diffs(diff_options), - project: merge_request.project, - diff_options: diff_options, - diff_refs: merge_request.diff_refs) - end - - private - - # - # If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted) - # for the highlighted ones, so we just skip their execution. - # If the highlighted diff files lines are not cached we calculate and cache them. - # - # The content of the cache is and Hash where the key correspond to the file_path and the values are Arrays of - # hashes than represent serialized diff lines. - # - def cache_highlight!(diff_files) - highlighted_cache = Rails.cache.read(cache_key) || {} - highlighted_cache_was_empty = highlighted_cache.empty? - - diff_files.each do |diff_file| - file_path = diff_file.file_path - - if highlighted_cache[file_path] - highlight_diff_file_from_cache!(diff_file, highlighted_cache[file_path]) - else - highlight_diff_file!(diff_file) - highlighted_cache[file_path] = diff_file.diff_lines.map(&:to_hash) - end - end - - if highlighted_cache_was_empty - Rails.cache.write(cache_key, highlighted_cache) - end - - diff_files - end - - def cacheable? - @merge_request.merge_request_diff.present? - end - - def cache_key - [@merge_request.merge_request_diff, 'highlighted-safe-diff-files', diff_options] - end - end -end diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb index 0a1905f7137..982540ba7f5 100644 --- a/app/services/merge_requests/merge_request_diff_cache_service.rb +++ b/app/services/merge_requests/merge_request_diff_cache_service.rb @@ -2,7 +2,7 @@ module MergeRequests class MergeRequestDiffCacheService def execute(merge_request) # Executing the iteration we cache all the highlighted diff information - SafeDiffs::MergeRequest.new(merge_request, diff_options: SafeDiffs.default_options).diff_files.to_a + merge_request.diff_file_collection(Gitlab::Diff::FileCollection.default_options).diff_files.to_a end end end diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml index 2d1a98caeaa..c161ecc3463 100644 --- a/app/views/notify/repository_push_email.html.haml +++ b/app/views/notify/repository_push_email.html.haml @@ -75,7 +75,7 @@ - blob = diff_file.blob - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) %table.code.white - - diff_file.diff_lines.each do |line| + - diff_file.highlighted_diff_lines.each do |line| = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true - else No preview for this file type diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index ea33aa472a6..935433306ea 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -2,7 +2,7 @@ = nav_link(path: 'commit#show') do = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do Changes - %span.badge= @diffs.count + %span.badge= @diffs.size = nav_link(path: 'commit#builds') do = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Builds diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index a483927671e..5970b9abf2b 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -5,7 +5,7 @@ %table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' } - last_line = 0 - - diff_file.diff_lines.each do |line| + - diff_file.highlighted_diff_lines.each do |line| - last_line = line.new_pos = render "projects/diffs/line", line: line, diff_file: diff_file diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index ed2765356db..5b842dd9280 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,7 +1,6 @@ - if @merge_request_diff.collected? - - diffs = SafeDiffs::MergeRequest.new(@merge_request, diff_options: diff_options) - = render "projects/diffs/diffs", diff_files: diffs.diff_files, - diff_refs: diffs.diff_refs, project: diffs.project + = render "projects/diffs/diffs", diff_files: @diffs.diff_files, + diff_refs: @diffs.diff_refs, project: @diffs.project - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 605ec4f04e5..a3c34e02baa 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -142,7 +142,7 @@ class IrkerWorker def files_count(commit) files = "#{commit.diffs.real_size} file" - files += 's' if commit.diffs.count > 1 + files += 's' if commit.diffs.size > 1 files end -- cgit v1.2.3 From 1d0c7b74920a94e488e6a2c090abb3e525438053 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 27 Jul 2016 13:09:52 +0200 Subject: Introduce Compare model in the codebase. This object will manage Gitlab::Git::Compare instances --- app/controllers/projects/compare_controller.rb | 18 +++------ .../projects/merge_requests_controller.rb | 7 +--- app/models/commit.rb | 2 +- app/models/compare.rb | 47 +++++++++++++++++++++- app/models/merge_request.rb | 8 +++- app/services/compare_service.rb | 7 +++- app/services/merge_requests/build_service.rb | 2 +- .../merge_request_diff_cache_service.rb | 2 +- app/views/projects/commit/show.html.haml | 2 +- app/views/projects/compare/show.html.haml | 2 +- app/views/projects/diffs/_diffs.html.haml | 7 ++-- .../projects/merge_requests/_new_submit.html.haml | 2 +- .../projects/merge_requests/show/_diffs.html.haml | 2 +- app/workers/emails_on_push_worker.rb | 20 ++++----- 14 files changed, 82 insertions(+), 46 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 252ddfa429a..7fca5e77f32 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(Compare.decorate(@compare, @project).diff_file_collection(diff_options: diff_options)) + render_diff_for_path(@compare.diff_file_collection(diff_options: diff_options)) end def create @@ -40,18 +40,12 @@ class Projects::CompareController < Projects::ApplicationController @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref) if @compare - @commits = Commit.decorate(@compare.commits, @project) + @commits = @compare.commits + @start_commit = @compare.start_commit + @commit = @compare.commit + @base_commit = @compare.base_commit - @start_commit = @project.commit(@start_ref) - @commit = @project.commit(@head_ref) - @base_commit = @project.merge_base_commit(@start_ref, @head_ref) - - diff_refs = Gitlab::Diff::DiffRefs.new( - base_sha: @base_commit.try(:sha), - start_sha: @start_commit.try(:sha), - head_sha: @commit.try(:sha) - ) - @diffs = Compare.decorate(@compare, @project).diff_file_collection(diff_options: diff_options, diff_refs: diff_refs) + @diffs = @compare.diff_file_collection(diff_options: diff_options) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 39e7d0f6182..20afc6afcb2 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -86,7 +86,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html { define_discussion_vars } format.json do - @diffs = @merge_request.diff_file_collection(diff_options) if @merge_request_diff.collected? + @diffs = @merge_request.diff_file_collection(diff_options) render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } end @@ -157,10 +157,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit if @merge_request.compare - @diffs = Compare.decorate(@merge_request.compare, @project).diff_file_collection( - diff_options: diff_options, - diff_refs: @merge_request.diff_refs - ) + @diffs = @merge_request.diff_file_collection(diff_options) end @diff_notes_disabled = true diff --git a/app/models/commit.rb b/app/models/commit.rb index d22ecb222e5..a339d47f5f3 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -317,7 +317,7 @@ class Commit nil end - def diff_file_collection(diff_options) + def diff_file_collection(diff_options = nil) Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) end diff --git a/app/models/compare.rb b/app/models/compare.rb index 6672d1bf059..05c8fbbcc36 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -1,5 +1,5 @@ class Compare - delegate :commits, :same, :head, :base, to: :@compare + delegate :same, :head, :base, to: :@compare def self.decorate(compare, project) if compare.is_a?(Compare) @@ -14,10 +14,53 @@ class Compare @project = project end - def diff_file_collection(diff_options:, diff_refs: nil) + def commits + @commits ||= Commit.decorate(@compare.commits, @project) + end + + def start_commit + return @start_commit if defined?(@start_commit) + + commit = @compare.base + @start_commit = commit ? ::Commit.new(commit, @project) : nil + end + + def commit + return @commit if defined?(@commit) + + commit = @compare.head + @commit = commit ? ::Commit.new(commit, @project) : nil + end + alias_method :head_commit, :commit + + # Used only on emails_on_push_worker.rb + def base_commit=(commit) + @base_commit = commit + end + + def base_commit + return @base_commit if defined?(@base_commit) + + @base_commit = if start_commit && commit + @project.merge_base_commit(start_commit.id, commit.id) + else + nil + end + end + + # keyword args until we get ride of diff_refs as argument + def diff_file_collection(diff_options:, diff_refs: self.diff_refs) Gitlab::Diff::FileCollection::Compare.new(@compare, project: @project, diff_options: diff_options, diff_refs: diff_refs) end + + def diff_refs + @diff_refs ||= Gitlab::Diff::DiffRefs.new( + base_sha: base_commit.try(:sha), + start_sha: start_commit.try(:sha), + head_sha: commit.try(:sha) + ) + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index abc8bacbe59..62e5573dfdc 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -168,8 +168,12 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args) end - def diff_file_collection(diff_options) - Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) + def diff_file_collection(diff_options = nil) + if self.compare + self.compare.diff_file_collection(diff_options: diff_options, diff_refs: diff_refs) + else + Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) + end end def diff_size diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 149822aa647..bb3aff72b47 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -20,10 +20,13 @@ class CompareService ) end - Gitlab::Git::Compare.new( + raw_compare = Gitlab::Git::Compare.new( target_project.repository.raw_repository, target_branch, - source_sha, + source_sha ) + + # REVIEW be sure if it's target_project or source_project + Compare.new(raw_compare, target_project) end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 7fe57747265..290742f1506 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -34,7 +34,7 @@ module MergeRequests # At this point we decide if merge request can be created # If we have at least one commit to merge -> creation allowed if commits.present? - merge_request.compare_commits = Commit.decorate(commits, merge_request.source_project) + merge_request.compare_commits = commits merge_request.can_be_created = true merge_request.compare = compare else diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb index 982540ba7f5..8151c24d1b0 100644 --- a/app/services/merge_requests/merge_request_diff_cache_service.rb +++ b/app/services/merge_requests/merge_request_diff_cache_service.rb @@ -2,7 +2,7 @@ module MergeRequests class MergeRequestDiffCacheService def execute(merge_request) # Executing the iteration we cache all the highlighted diff information - merge_request.diff_file_collection(Gitlab::Diff::FileCollection.default_options).diff_files.to_a + merge_request.diff_file_collection.diff_files.to_a end end end diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 11b2020f99b..ed44d86a687 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -7,7 +7,7 @@ = render "ci_menu" - else %div.block-connector -= render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs += render "projects/diffs/diffs", diffs: @diffs = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index eb8a1bd5289..819e9bc15ae 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -8,7 +8,7 @@ - if @commits.present? = render "projects/commits/commit_list" - = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs + = render "projects/diffs/diffs", diffs: @diffs - else .light-well .center diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 45895a9a3de..35fdf7d5278 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,4 +1,5 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) +- diff_files = diffs.diff_files - if diff_view == 'parallel' - fluid_layout true @@ -26,7 +27,7 @@ - diff_commit = commit_for_diff(diff_file) - blob = diff_file.blob(diff_commit) - next unless blob - - blob.load_all_data!(project.repository) unless blob.only_display_raw? + - blob.load_all_data!(@project.repository) unless blob.only_display_raw? - = render 'projects/diffs/file', i: index, project: project, - diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diff_refs + = render 'projects/diffs/file', i: index, project: @project, + diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diffs.diff_refs diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index cb2b623691c..598bd743676 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -42,7 +42,7 @@ %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. %p To preserve performance the line changes are not shown. - else - = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false + = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false - if @pipeline #builds.builds.tab-pane = render "projects/merge_requests/show/builds" diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 5b842dd9280..c6d2567af35 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diff_files: @diffs.diff_files, + = render "projects/diffs/diffs", diffs: @diffs, diff_refs: @diffs.diff_refs, project: @diffs.project - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 0b6a01a3200..0b63913cfd1 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,25 +33,19 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - merge_base_sha = project.merge_base_commit(before_sha, after_sha).try(:sha) + base_commit = project.merge_base_commit(before_sha, after_sha) compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - - diff_refs = Gitlab::Diff::DiffRefs.new( - base_sha: merge_base_sha, - start_sha: before_sha, - head_sha: after_sha - ) + compare = Compare.decorate(compare, project) + compare.base_commit = base_commit + diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) - - diff_refs = Gitlab::Diff::DiffRefs.new( - base_sha: merge_base_sha, - start_sha: after_sha, - head_sha: before_sha - ) + compare = Compare.decorate(compare, project) + compare.base_commit = base_commit + diff_refs = compare.diff_refs reverse_compare = true -- cgit v1.2.3 From c86c1905b5574cac234315598d8d715fcaee3ea7 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 27 Jul 2016 19:00:34 +0200 Subject: switch from diff_file_collection to diffs So we have raw_diffs too --- app/controllers/projects/commit_controller.rb | 4 +-- app/controllers/projects/compare_controller.rb | 4 +-- .../projects/merge_requests_controller.rb | 8 ++--- app/helpers/diff_helper.rb | 5 +-- app/models/commit.rb | 10 ++++-- app/models/compare.rb | 36 +++++++++++----------- app/models/legacy_diff_note.rb | 4 +-- app/models/merge_request.rb | 8 ++--- app/models/repository.rb | 2 +- app/services/compare_service.rb | 1 - .../merge_request_diff_cache_service.rb | 2 +- app/views/projects/diffs/_diffs.html.haml | 14 ++++----- .../projects/merge_requests/show/_diffs.html.haml | 3 +- app/workers/emails_on_push_worker.rb | 9 ++---- app/workers/irker_worker.rb | 6 ++-- 15 files changed, 55 insertions(+), 61 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 771a86530cd..fdfe7c65b7b 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -28,7 +28,7 @@ class Projects::CommitController < Projects::ApplicationController end def diff_for_path - render_diff_for_path(@commit.diff_file_collection(diff_options)) + render_diff_for_path(@commit.diffs(diff_options)) end def builds @@ -110,7 +110,7 @@ class Projects::CommitController < Projects::ApplicationController opts = diff_options opts[:ignore_whitespace_change] = true if params[:format] == 'diff' - @diffs = commit.diff_file_collection(opts) + @diffs = commit.diffs(opts) @notes_count = commit.notes.count end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 7fca5e77f32..4a42a7d091b 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(@compare.diff_file_collection(diff_options: diff_options)) + render_diff_for_path(@compare.diffs(diff_options: diff_options)) end def create @@ -45,7 +45,7 @@ class Projects::CompareController < Projects::ApplicationController @commit = @compare.commit @base_commit = @compare.base_commit - @diffs = @compare.diff_file_collection(diff_options: diff_options) + @diffs = @compare.diffs(diff_options: diff_options) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 20afc6afcb2..2cf6a2dd1b3 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -86,7 +86,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html { define_discussion_vars } format.json do - @diffs = @merge_request.diff_file_collection(diff_options) + @diffs = @merge_request.diffs(diff_options) render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } end @@ -108,7 +108,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController define_commit_vars - render_diff_for_path(@merge_request.diff_file_collection(diff_options)) + render_diff_for_path(@merge_request.diffs(diff_options)) end def commits @@ -156,9 +156,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits = @merge_request.compare_commits.reverse @commit = @merge_request.diff_head_commit @base_commit = @merge_request.diff_base_commit - if @merge_request.compare - @diffs = @merge_request.diff_file_collection(diff_options) - end + @diffs = @merge_request.diffs(diff_options) if @merge_request.compare @diff_notes_disabled = true @pipeline = @merge_request.pipeline diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 2abe24b78bf..cc7121b1163 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -23,10 +23,7 @@ module DiffHelper end def diff_options - options = Gitlab::Diff::FileCollection.default_options.merge( - ignore_whitespace_change: hide_whitespace?, - no_collapse: expand_all_diffs? - ) + options = { ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? } if action_name == 'diff_for_path' options[:no_collapse] = true diff --git a/app/models/commit.rb b/app/models/commit.rb index a339d47f5f3..d58c2fb8106 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -104,7 +104,7 @@ class Commit end def diff_line_count - @diff_line_count ||= Commit::diff_line_count(self.diffs) + @diff_line_count ||= Commit::diff_line_count(raw_diffs) @diff_line_count end @@ -317,7 +317,11 @@ class Commit nil end - def diff_file_collection(diff_options = nil) + def raw_diffs(*args) + raw.diffs(*args) + end + + def diffs(diff_options = nil) Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) end @@ -330,7 +334,7 @@ class Commit def repo_changes changes = { added: [], modified: [], removed: [] } - diffs.each do |diff| + raw_diffs.each do |diff| if diff.deleted_file changes[:removed] << diff.old_path elsif diff.renamed_file || diff.new_file diff --git a/app/models/compare.rb b/app/models/compare.rb index 05c8fbbcc36..98c042f3809 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -1,6 +1,8 @@ class Compare delegate :same, :head, :base, to: :@compare + attr_reader :project + def self.decorate(compare, project) if compare.is_a?(Compare) compare @@ -15,49 +17,47 @@ class Compare end def commits - @commits ||= Commit.decorate(@compare.commits, @project) + @commits ||= Commit.decorate(@compare.commits, project) end def start_commit return @start_commit if defined?(@start_commit) commit = @compare.base - @start_commit = commit ? ::Commit.new(commit, @project) : nil + @start_commit = commit ? ::Commit.new(commit, project) : nil end - def commit - return @commit if defined?(@commit) + def head_commit + return @head_commit if defined?(@head_commit) commit = @compare.head - @commit = commit ? ::Commit.new(commit, @project) : nil - end - alias_method :head_commit, :commit - - # Used only on emails_on_push_worker.rb - def base_commit=(commit) - @base_commit = commit + @head_commit = commit ? ::Commit.new(commit, project) : nil end + alias_method :commit, :head_commit def base_commit return @base_commit if defined?(@base_commit) - @base_commit = if start_commit && commit - @project.merge_base_commit(start_commit.id, commit.id) + @base_commit = if start_commit && head_commit + project.merge_base_commit(start_commit.id, head_commit.id) else nil end end - # keyword args until we get ride of diff_refs as argument - def diff_file_collection(diff_options:, diff_refs: self.diff_refs) - Gitlab::Diff::FileCollection::Compare.new(@compare, - project: @project, + def raw_diffs(*args) + @compare.diffs(*args) + end + + def diffs(diff_options:) + Gitlab::Diff::FileCollection::Compare.new(self, + project: project, diff_options: diff_options, diff_refs: diff_refs) end def diff_refs - @diff_refs ||= Gitlab::Diff::DiffRefs.new( + Gitlab::Diff::DiffRefs.new( base_sha: base_commit.try(:sha), start_sha: start_commit.try(:sha), head_sha: commit.try(:sha) diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 865712268a0..6ed66001513 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -85,7 +85,7 @@ class LegacyDiffNote < Note return nil unless noteable return @diff if defined?(@diff) - @diff = noteable.diffs(Commit.max_diff_options).find do |d| + @diff = noteable.raw_diffs(Commit.max_diff_options).find do |d| d.new_path && Digest::SHA1.hexdigest(d.new_path) == diff_file_hash end end @@ -116,7 +116,7 @@ class LegacyDiffNote < Note # Find the diff on noteable that matches our own def find_noteable_diff - diffs = noteable.diffs(Commit.max_diff_options) + diffs = noteable.raw_diffs(Commit.max_diff_options) diffs.find { |d| d.new_path == self.diff.new_path } end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 62e5573dfdc..009262d6b48 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -164,13 +164,13 @@ class MergeRequest < ActiveRecord::Base merge_request_diff ? merge_request_diff.first_commit : compare_commits.first end - def diffs(*args) - merge_request_diff ? merge_request_diff.diffs(*args) : compare.diffs(*args) + def raw_diffs(*args) + merge_request_diff ? merge_request_diff.diffs(*args) : compare.raw_diffs(*args) end - def diff_file_collection(diff_options = nil) + def diffs(diff_options = nil) if self.compare - self.compare.diff_file_collection(diff_options: diff_options, diff_refs: diff_refs) + self.compare.diffs(diff_options: diff_options) else Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) end diff --git a/app/models/repository.rb b/app/models/repository.rb index bac37483c47..3d95344a68f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -372,7 +372,7 @@ class Repository # We don't want to flush the cache if the commit didn't actually make any # changes to any of the possible avatar files. if revision && commit = self.commit(revision) - return unless commit.diffs. + return unless commit.raw_diffs. any? { |diff| AVATAR_FILES.include?(diff.new_path) } end diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index bb3aff72b47..6d6075628af 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -26,7 +26,6 @@ class CompareService source_sha ) - # REVIEW be sure if it's target_project or source_project Compare.new(raw_compare, target_project) end end diff --git a/app/services/merge_requests/merge_request_diff_cache_service.rb b/app/services/merge_requests/merge_request_diff_cache_service.rb index 8151c24d1b0..2945a7fd4e4 100644 --- a/app/services/merge_requests/merge_request_diff_cache_service.rb +++ b/app/services/merge_requests/merge_request_diff_cache_service.rb @@ -2,7 +2,7 @@ module MergeRequests class MergeRequestDiffCacheService def execute(merge_request) # Executing the iteration we cache all the highlighted diff information - merge_request.diff_file_collection.diff_files.to_a + merge_request.diffs.diff_files.to_a end end end diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 35fdf7d5278..20dc280c3b2 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -9,11 +9,11 @@ = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: nil)), class: 'btn btn-default' - if show_whitespace_toggle - if current_controller?(:commit) - = commit_diff_whitespace_link(@project, @commit, class: 'hidden-xs') + = commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs') - elsif current_controller?(:merge_requests) - = diff_merge_request_whitespace_link(@project, @merge_request, class: 'hidden-xs') + = diff_merge_request_whitespace_link(diffs.project, @merge_request, class: 'hidden-xs') - elsif current_controller?(:compare) - = diff_compare_whitespace_link(@project, params[:from], params[:to], class: 'hidden-xs') + = diff_compare_whitespace_link(diffs.project, params[:from], params[:to], class: 'hidden-xs') .btn-group = inline_diff_btn = parallel_diff_btn @@ -22,12 +22,12 @@ - if diff_files.overflow? = render 'projects/diffs/warning', diff_files: diff_files -.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, @project))}} +.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}} - diff_files.each_with_index do |diff_file, index| - diff_commit = commit_for_diff(diff_file) - blob = diff_file.blob(diff_commit) - next unless blob - - blob.load_all_data!(@project.repository) unless blob.only_display_raw? + - blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw? - = render 'projects/diffs/file', i: index, project: @project, - diff_file: diff_file, diff_commit: diff_commit, blob: blob, diff_refs: diffs.diff_refs + = render 'projects/diffs/file', i: index, project: diffs.project, + diff_file: diff_file, diff_commit: diff_commit, blob: blob diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index c6d2567af35..013b05628fa 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,6 +1,5 @@ - if @merge_request_diff.collected? - = render "projects/diffs/diffs", diffs: @diffs, - diff_refs: @diffs.diff_refs, project: @diffs.project + = render "projects/diffs/diffs", diffs: @diffs - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} - else diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 0b63913cfd1..c6a5af2809a 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,18 +33,13 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - base_commit = project.merge_base_commit(before_sha, after_sha) - compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha) - compare = Compare.decorate(compare, project) - compare.base_commit = base_commit + compare = CompareService.new.execute(project, before_sha, project, after_sha) diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? - compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha) - compare = Compare.decorate(compare, project) - compare.base_commit = base_commit + compare = CompareService.new.execute(project, after_sha, project, before_sha) diff_refs = compare.diff_refs reverse_compare = true diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index a3c34e02baa..07cc7c1cbd7 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -141,8 +141,10 @@ class IrkerWorker end def files_count(commit) - files = "#{commit.diffs.real_size} file" - files += 's' if commit.diffs.size > 1 + diffs = commit.raw_diffs + + files = "#{diffs.real_size} file" + files += 's' if diffs.size > 1 files end -- cgit v1.2.3 From 62f115dd25c4d3639dceac1b3b81c9fe42eeedd3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 3 Aug 2016 14:35:11 +0800 Subject: Introduce execute_hooks_unless_ci_skipped --- app/models/ci/pipeline.rb | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 4e6ccf48c68..f8506e33295 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -18,6 +18,7 @@ module Ci # Invalidate object and save if when touched after_touch :update_state + after_touch :execute_hooks_unless_ci_skipped after_save :keep_around_commits # ref can't be HEAD or SHA, can only be branch/tag name @@ -237,9 +238,11 @@ module Ci self.started_at = statuses.started_at self.finished_at = statuses.finished_at self.duration = statuses.latest.duration - saved = save - execute_hooks if saved && !skip_ci? - saved + save + end + + def execute_hooks_unless_ci_skipped + execute_hooks unless skip_ci? end def execute_hooks -- cgit v1.2.3 From b1b32bfcf8c28e038a7450238a4109288e1146ed Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 3 Aug 2016 09:31:36 +0100 Subject: Fixed issue with `this` not being defined --- app/assets/javascripts/gl_dropdown.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index a9a7c802f87..356810c85cc 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -60,8 +60,8 @@ } return this.options.query(this.input.val(), function(data) { return this.options.callback(data); - }); - }, 250); + }.bind(this)); + }.bind(this), 250); } else { return this.filter(this.input.val()); } -- cgit v1.2.3 From db123d29e73419e9a3e3fc3afe03fd626ae9d776 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 3 Aug 2016 17:33:34 +0800 Subject: More descriptive comments, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5620#note_13539571 --- app/controllers/concerns/service_params.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index 58877c5ad5d..a69877edfd4 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -7,8 +7,12 @@ module ServiceParams :build_key, :server, :teamcity_url, :drone_url, :build_type, :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :colorize_messages, :channels, - # See app/helpers/services_helper.rb - # for why we need issues_events and merge_requests_events. + # We're using `issues_events` and `merge_requests_events` + # in the view so we still need to explicitly state them + # here. `Service#event_names` would only give + # `issue_events` and `merge_request_events` (singular!) + # See app/helpers/services_helper.rb for how we + # make those event names plural as special case. :issues_events, :merge_requests_events, :notify_only_broken_builds, :notify_only_broken_pipelines, :add_pusher, :send_from_committer_email, :disable_diffs, -- cgit v1.2.3 From b8f754dd0abdf437669e17a820a8e6c230afa73e Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 3 Aug 2016 14:54:12 +0200 Subject: Stop 'git push' over HTTP early Before this change we always let users push Git data over HTTP before deciding whether to accept to push. This was different from pushing over SSH where we terminate a 'git push' early if we already know the user is not allowed to push. This change let Git over HTTP follow the same behavior as Git over SSH. We also distinguish between HTTP 404 and 403 responses when denying Git requests, depending on whether the user is allowed to know the project exists. --- app/controllers/projects/git_http_controller.rb | 39 ++++++++++++++----------- 1 file changed, 22 insertions(+), 17 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 40a8b7940d9..e2f93e239bd 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -20,9 +20,9 @@ class Projects::GitHttpController < Projects::ApplicationController elsif receive_pack? && receive_pack_allowed? render_ok elsif http_blocked? - render_not_allowed + render_http_not_allowed else - render_not_found + render_denied end end @@ -31,7 +31,7 @@ class Projects::GitHttpController < Projects::ApplicationController if upload_pack? && upload_pack_allowed? render_ok else - render_not_found + render_denied end end @@ -40,7 +40,7 @@ class Projects::GitHttpController < Projects::ApplicationController if receive_pack? && receive_pack_allowed? render_ok else - render_not_found + render_denied end end @@ -156,8 +156,17 @@ class Projects::GitHttpController < Projects::ApplicationController render plain: 'Not Found', status: :not_found end - def render_not_allowed - render plain: download_access.message, status: :forbidden + def render_http_not_allowed + render plain: access_check.message, status: :forbidden + end + + def render_denied + if user && user.can?(:read_project, project) + render plain: 'Access denied', status: :forbidden + else + # Do not leak information about project existence + render_not_found + end end def ci? @@ -168,22 +177,20 @@ class Projects::GitHttpController < Projects::ApplicationController return false unless Gitlab.config.gitlab_shell.upload_pack if user - download_access.allowed? + access_check.allowed? else ci? || project.public? end end def access - return @access if defined?(@access) - - @access = Gitlab::GitAccess.new(user, project, 'http') + @access ||= Gitlab::GitAccess.new(user, project, 'http') end - def download_access - return @download_access if defined?(@download_access) - - @download_access = access.check('git-upload-pack') + def access_check + # Use the magic string '_any' to indicate we do not know what the + # changes are. This is also what gitlab-shell does. + @access_check ||= access.check(git_command, '_any') end def http_blocked? @@ -193,8 +200,6 @@ class Projects::GitHttpController < Projects::ApplicationController def receive_pack_allowed? return false unless Gitlab.config.gitlab_shell.receive_pack - # Skip user authorization on upload request. - # It will be done by the pre-receive hook in the repository. - user.present? + access_check.allowed? end end -- cgit v1.2.3 From 405379bbfcb7821b3dae77e5254362f2d696bb7d Mon Sep 17 00:00:00 2001 From: Sean McGivern Date: Fri, 15 Jul 2016 13:19:29 +0100 Subject: Store OTP secret key in secrets.yml .secret stores the secret token used for both encrypting login cookies and for encrypting stored OTP secrets. We can't rotate this, because that would invalidate all existing OTP secrets. If the secret token is present in the .secret file or an environment variable, save it as otp_key_base in secrets.yml. Now .secret can be rotated without invalidating OTP secrets. If the secret token isn't present (initial setup), then just generate a separate otp_key_base and save in secrets.yml. Update the docs to reflect that secrets.yml needs to be retained past upgrades, but .secret doesn't. --- app/models/user.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/user.rb b/app/models/user.rb index db747434959..73368be7b1b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -23,13 +23,13 @@ class User < ActiveRecord::Base default_value_for :theme_id, gitlab_config.default_theme attr_encrypted :otp_secret, - key: Gitlab::Application.config.secret_key_base, + key: Gitlab::Application.secrets.otp_key_base, mode: :per_attribute_iv_and_salt, insecure_mode: true, algorithm: 'aes-256-cbc' devise :two_factor_authenticatable, - otp_secret_encryption_key: Gitlab::Application.config.secret_key_base + otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base devise :two_factor_backupable, otp_number_of_backup_codes: 10 serialize :otp_backup_codes, JSON -- cgit v1.2.3 From 3691a391524911a21d5af1c75cb4cd16a8a6e475 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 3 Aug 2016 23:06:28 +0800 Subject: We don't have to touch it because builds would touch pipeline anyway --- app/services/ci/create_pipeline_service.rb | 1 - app/services/create_commit_builds_service.rb | 10 ---------- 2 files changed, 11 deletions(-) (limited to 'app') diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 7a8b0683acb..be91bf0db85 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,7 +27,6 @@ module Ci end pipeline.save! - pipeline.touch unless pipeline.create_builds(current_user) pipeline.errors.add(:base, 'No builds for this pipeline.') diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 0b66b854dea..5e77768abe7 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -53,17 +53,7 @@ class CreateCommitBuildsService return false end - save_pipeline! - end - - private - - ## - # Create a new pipeline and touch object to calculate status - # - def save_pipeline! @pipeline.save! - @pipeline.touch @pipeline end end -- cgit v1.2.3 From 9e06bde269b80b3af01b7cc00bdada1ce2e5e563 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 3 Aug 2016 23:39:14 +0800 Subject: Make sure we only fire hooks upon status changed --- app/models/ci/pipeline.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f8506e33295..822ba7b6c00 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -18,7 +18,7 @@ module Ci # Invalidate object and save if when touched after_touch :update_state - after_touch :execute_hooks_unless_ci_skipped + after_save :execute_hooks_if_status_changed after_save :keep_around_commits # ref can't be HEAD or SHA, can only be branch/tag name @@ -241,8 +241,8 @@ module Ci save end - def execute_hooks_unless_ci_skipped - execute_hooks unless skip_ci? + def execute_hooks_if_status_changed + execute_hooks if status_changed? && !skip_ci? end def execute_hooks -- cgit v1.2.3 From c008a1a9674f7c01b4504e22ed414b07eff05385 Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Wed, 3 Aug 2016 09:32:01 -0700 Subject: Make Compare#diffs diff_options a regular argument --- app/controllers/projects/compare_controller.rb | 4 ++-- app/models/compare.rb | 2 +- app/models/merge_request.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 4a42a7d091b..bee3d56076c 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -21,7 +21,7 @@ class Projects::CompareController < Projects::ApplicationController def diff_for_path return render_404 unless @compare - render_diff_for_path(@compare.diffs(diff_options: diff_options)) + render_diff_for_path(@compare.diffs(diff_options)) end def create @@ -45,7 +45,7 @@ class Projects::CompareController < Projects::ApplicationController @commit = @compare.commit @base_commit = @compare.base_commit - @diffs = @compare.diffs(diff_options: diff_options) + @diffs = @compare.diffs(diff_options) @diff_notes_disabled = true @grouped_diff_discussions = {} diff --git a/app/models/compare.rb b/app/models/compare.rb index 98c042f3809..4856510f526 100644 --- a/app/models/compare.rb +++ b/app/models/compare.rb @@ -49,7 +49,7 @@ class Compare @compare.diffs(*args) end - def diffs(diff_options:) + def diffs(diff_options = nil) Gitlab::Diff::FileCollection::Compare.new(self, project: project, diff_options: diff_options, diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 009262d6b48..c4761fac2fb 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -170,7 +170,7 @@ class MergeRequest < ActiveRecord::Base def diffs(diff_options = nil) if self.compare - self.compare.diffs(diff_options: diff_options) + self.compare.diffs(diff_options) else Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options) end -- cgit v1.2.3 From 94b3d33de1417b31ef3994e43f901941dc302ca0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 4 Aug 2016 00:46:58 +0800 Subject: If we use Rails magic it's breaking this test: spec/lib/gitlab/data_builder/pipeline_data_builder_spec.rb Because it would trigger the event just after saved and it would load no builds and cache it. We should really avoid adding more magic. --- app/models/ci/pipeline.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 822ba7b6c00..ca41a998a2b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -18,7 +18,6 @@ module Ci # Invalidate object and save if when touched after_touch :update_state - after_save :execute_hooks_if_status_changed after_save :keep_around_commits # ref can't be HEAD or SHA, can only be branch/tag name @@ -230,6 +229,7 @@ module Ci def update_state statuses.reload + last_status = status self.status = if yaml_errors.blank? statuses.latest.status || 'skipped' else @@ -238,11 +238,9 @@ module Ci self.started_at = statuses.started_at self.finished_at = statuses.finished_at self.duration = statuses.latest.duration - save - end - - def execute_hooks_if_status_changed - execute_hooks if status_changed? && !skip_ci? + saved = save + execute_hooks if last_status != status && saved && !skip_ci? + saved end def execute_hooks -- cgit v1.2.3 From 431792f78404c4b83aa8c962ff306f4aacd35f8b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 4 Aug 2016 01:05:17 +0800 Subject: Revert "We don't have to touch it because builds would touch pipeline anyway" This reverts commit 3691a391524911a21d5af1c75cb4cd16a8a6e475. --- app/services/ci/create_pipeline_service.rb | 1 + app/services/create_commit_builds_service.rb | 10 ++++++++++ 2 files changed, 11 insertions(+) (limited to 'app') diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index be91bf0db85..7a8b0683acb 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,6 +27,7 @@ module Ci end pipeline.save! + pipeline.touch unless pipeline.create_builds(current_user) pipeline.errors.add(:base, 'No builds for this pipeline.') diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb index 5e77768abe7..0b66b854dea 100644 --- a/app/services/create_commit_builds_service.rb +++ b/app/services/create_commit_builds_service.rb @@ -53,7 +53,17 @@ class CreateCommitBuildsService return false end + save_pipeline! + end + + private + + ## + # Create a new pipeline and touch object to calculate status + # + def save_pipeline! @pipeline.save! + @pipeline.touch @pipeline end end -- cgit v1.2.3 From 80671bf75cdac3f50615253b058fa04da6235a4f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 4 Aug 2016 01:18:33 +0800 Subject: Separate the concern for executing hooks and updating states --- app/models/ci/pipeline.rb | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index ca41a998a2b..81991e8aa60 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -228,8 +228,18 @@ module Ci end def update_state - statuses.reload last_status = status + + if update_state_from_commit_statuses + execute_hooks if last_status != status && !skip_ci? + true + else + false + end + end + + def update_state_from_commit_statuses + statuses.reload self.status = if yaml_errors.blank? statuses.latest.status || 'skipped' else @@ -238,9 +248,7 @@ module Ci self.started_at = statuses.started_at self.finished_at = statuses.finished_at self.duration = statuses.latest.duration - saved = save - execute_hooks if last_status != status && saved && !skip_ci? - saved + save end def execute_hooks -- cgit v1.2.3 From a16c26c957ae893f6957fd0ad66c189d0b8ca079 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Fri, 29 Jul 2016 19:31:37 +0200 Subject: Speed up Commit#repo_changes --- app/models/commit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/commit.rb b/app/models/commit.rb index d58c2fb8106..cc413448ce8 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -334,7 +334,7 @@ class Commit def repo_changes changes = { added: [], modified: [], removed: [] } - raw_diffs.each do |diff| + raw_diffs(deltas_only: true).each do |diff| if diff.deleted_file changes[:removed] << diff.old_path elsif diff.renamed_file || diff.new_file -- cgit v1.2.3 From 6eba7188f1cd1fc0bfcb8b1cf46f40338dc892b5 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Fri, 29 Jul 2016 13:46:39 -0700 Subject: Use only deltas in diffs when scanning the last commit for changes in the avatar to save memory --- app/models/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/repository.rb b/app/models/repository.rb index 3d95344a68f..c1170c470ea 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -372,7 +372,7 @@ class Repository # We don't want to flush the cache if the commit didn't actually make any # changes to any of the possible avatar files. if revision && commit = self.commit(revision) - return unless commit.raw_diffs. + return unless commit.raw_diffs(deltas_only: true). any? { |diff| AVATAR_FILES.include?(diff.new_path) } end -- cgit v1.2.3 From 08c1dd348273df67bf14172e9082308e12f94784 Mon Sep 17 00:00:00 2001 From: Ahmad Sherif Date: Mon, 1 Aug 2016 13:14:41 +0200 Subject: Use commit deltas when counting files in IrkerWorker --- app/workers/irker_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/workers/irker_worker.rb b/app/workers/irker_worker.rb index 07cc7c1cbd7..19f38358eb5 100644 --- a/app/workers/irker_worker.rb +++ b/app/workers/irker_worker.rb @@ -141,7 +141,7 @@ class IrkerWorker end def files_count(commit) - diffs = commit.raw_diffs + diffs = commit.raw_diffs(deltas_only: true) files = "#{diffs.real_size} file" files += 's' if diffs.size > 1 -- cgit v1.2.3 From e8c6f119cd7cf519db3ad1622786aad3156324d4 Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 18 Jul 2016 15:14:30 -0600 Subject: Add an oauth provider path helper. The helper constructs the path for a given oauth provider since Devise 4.0 deprecated passing the provider to the omniauth authentication path. Fixes #18110. --- app/controllers/sessions_controller.rb | 2 +- app/helpers/auth_helper.rb | 6 ++++++ app/views/devise/sessions/_new_crowd.html.haml | 2 +- app/views/devise/shared/_omniauth_box.html.haml | 2 +- app/views/profiles/accounts/show.html.haml | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 17aed816cbd..25598e065b8 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -101,7 +101,7 @@ class SessionsController < Devise::SessionsController # Prevent alert from popping up on the first page shown after authentication. flash[:alert] = nil - redirect_to user_omniauth_authorize_path(provider.to_sym) + redirect_to provider_path(provider) end def valid_otp_attempt?(user) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index cd4d778e508..dcc156311a1 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -60,6 +60,12 @@ module AuthHelper end end + # Constructs the OAuth provider path. + # For example: user_google_omniauth_authorize_path + def provider_path(provider) + send("user_#{provider.underscore}_omniauth_authorize_path") + end + def auth_active?(provider) current_user.identities.exists?(provider: provider.to_s) end diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index 8e81671b7e7..6c7c89700cd 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -1,4 +1,4 @@ -= form_tag(user_omniauth_authorize_path("crowd"), id: 'new_crowd_user' ) do += form_tag(provider_path("crowd"), id: 'new_crowd_user' ) do = text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} - if devise_mapping.rememberable? diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index de18bc2d844..07d4caf6b94 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -5,4 +5,4 @@ - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" + = link_to provider_image_tag(provider), provider_path(provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 57d16d29158..7fadd7bf9f1 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -70,7 +70,7 @@ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do Disconnect - else - = link_to user_omniauth_authorize_path(provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do + = link_to provider_path(provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do Connect %hr - if current_user.can_change_username? -- cgit v1.2.3 From 602fe111912bac119e752b0dfa3b4b3cd81585ff Mon Sep 17 00:00:00 2001 From: Connor Shea Date: Mon, 25 Jul 2016 11:40:40 -0600 Subject: Remove provider path, replace with dynamic path. --- app/controllers/sessions_controller.rb | 2 +- app/helpers/auth_helper.rb | 6 ------ app/views/devise/sessions/_new_crowd.html.haml | 2 +- app/views/devise/shared/_omniauth_box.html.haml | 2 +- app/views/profiles/accounts/show.html.haml | 2 +- 5 files changed, 4 insertions(+), 10 deletions(-) (limited to 'app') diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 25598e065b8..5d7ecfeacf4 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -101,7 +101,7 @@ class SessionsController < Devise::SessionsController # Prevent alert from popping up on the first page shown after authentication. flash[:alert] = nil - redirect_to provider_path(provider) + redirect_to omniauth_authorize_path(:user, provider) end def valid_otp_attempt?(user) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index dcc156311a1..cd4d778e508 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -60,12 +60,6 @@ module AuthHelper end end - # Constructs the OAuth provider path. - # For example: user_google_omniauth_authorize_path - def provider_path(provider) - send("user_#{provider.underscore}_omniauth_authorize_path") - end - def auth_active?(provider) current_user.identities.exists?(provider: provider.to_s) end diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index 6c7c89700cd..b7d3acac2b1 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -1,4 +1,4 @@ -= form_tag(provider_path("crowd"), id: 'new_crowd_user' ) do += form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' ) do = text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"} = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} - if devise_mapping.rememberable? diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 07d4caf6b94..2e7da2747d0 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -5,4 +5,4 @@ - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), provider_path(provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" + = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 7fadd7bf9f1..c80f22457b4 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -70,7 +70,7 @@ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do Disconnect - else - = link_to provider_path(provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do Connect %hr - if current_user.can_change_username? -- cgit v1.2.3 From c887a131c018d02d3d71bea22d02934ca96d4acb Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Mon, 1 Aug 2016 14:16:53 -0500 Subject: Remove unused images --- app/assets/images/bg-header.png | Bin 90 -> 0 bytes app/assets/images/bg_fallback.png | Bin 167 -> 0 bytes app/assets/images/chosen-sprite.png | Bin 367 -> 0 bytes app/assets/images/diff_note_add.png | Bin 418 -> 0 bytes app/assets/images/icon-search.png | Bin 222 -> 0 bytes app/assets/images/icon_sprite.png | Bin 2636 -> 0 bytes app/assets/images/images.png | Bin 5806 -> 0 bytes app/assets/images/move.png | Bin 197 -> 0 bytes app/assets/images/progress_bar.gif | Bin 494 -> 0 bytes app/assets/images/slider_handles.png | Bin 1341 -> 0 bytes 10 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/assets/images/bg-header.png delete mode 100644 app/assets/images/bg_fallback.png delete mode 100644 app/assets/images/chosen-sprite.png delete mode 100644 app/assets/images/diff_note_add.png delete mode 100644 app/assets/images/icon-search.png delete mode 100644 app/assets/images/icon_sprite.png delete mode 100644 app/assets/images/images.png delete mode 100644 app/assets/images/move.png delete mode 100644 app/assets/images/progress_bar.gif delete mode 100644 app/assets/images/slider_handles.png (limited to 'app') diff --git a/app/assets/images/bg-header.png b/app/assets/images/bg-header.png deleted file mode 100644 index 639271c6faf..00000000000 Binary files a/app/assets/images/bg-header.png and /dev/null differ diff --git a/app/assets/images/bg_fallback.png b/app/assets/images/bg_fallback.png deleted file mode 100644 index 5c55bc79dec..00000000000 Binary files a/app/assets/images/bg_fallback.png and /dev/null differ diff --git a/app/assets/images/chosen-sprite.png b/app/assets/images/chosen-sprite.png deleted file mode 100644 index 3d936b07d44..00000000000 Binary files a/app/assets/images/chosen-sprite.png and /dev/null differ diff --git a/app/assets/images/diff_note_add.png b/app/assets/images/diff_note_add.png deleted file mode 100644 index 0084422e330..00000000000 Binary files a/app/assets/images/diff_note_add.png and /dev/null differ diff --git a/app/assets/images/icon-search.png b/app/assets/images/icon-search.png deleted file mode 100644 index 3c1c146541d..00000000000 Binary files a/app/assets/images/icon-search.png and /dev/null differ diff --git a/app/assets/images/icon_sprite.png b/app/assets/images/icon_sprite.png deleted file mode 100644 index 2e7a5023398..00000000000 Binary files a/app/assets/images/icon_sprite.png and /dev/null differ diff --git a/app/assets/images/images.png b/app/assets/images/images.png deleted file mode 100644 index bd60de994c4..00000000000 Binary files a/app/assets/images/images.png and /dev/null differ diff --git a/app/assets/images/move.png b/app/assets/images/move.png deleted file mode 100644 index 6a0567f8f25..00000000000 Binary files a/app/assets/images/move.png and /dev/null differ diff --git a/app/assets/images/progress_bar.gif b/app/assets/images/progress_bar.gif deleted file mode 100644 index c3d43fa40b2..00000000000 Binary files a/app/assets/images/progress_bar.gif and /dev/null differ diff --git a/app/assets/images/slider_handles.png b/app/assets/images/slider_handles.png deleted file mode 100644 index 52ad11ab7a1..00000000000 Binary files a/app/assets/images/slider_handles.png and /dev/null differ -- cgit v1.2.3 From 631f59d4e7f449c59735cd7eab25cf407e06d5d8 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 3 Aug 2016 23:32:12 +0200 Subject: change the API on the merge_request_diff model from diffs -> raw_diffs --- app/models/merge_request.rb | 2 +- app/models/merge_request_diff.rb | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c4761fac2fb..b1fb3ce5d69 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -165,7 +165,7 @@ class MergeRequest < ActiveRecord::Base end def raw_diffs(*args) - merge_request_diff ? merge_request_diff.diffs(*args) : compare.raw_diffs(*args) + merge_request_diff ? merge_request_diff.raw_diffs(*args) : compare.raw_diffs(*args) end def diffs(diff_options = nil) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 119266f2d2c..fa0efe2d596 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -33,12 +33,12 @@ class MergeRequestDiff < ActiveRecord::Base end def size - real_size.presence || diffs.size + real_size.presence || raw_diffs.size end - def diffs(options={}) + def raw_diffs(options={}) if options[:ignore_whitespace_change] - @diffs_no_whitespace ||= begin + @raw_diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( repository.raw_repository, self.start_commit_sha || self.target_branch_sha, @@ -47,8 +47,8 @@ class MergeRequestDiff < ActiveRecord::Base compare.diffs(options) end else - @diffs ||= {} - @diffs[options] ||= load_diffs(st_diffs, options) + @raw_diffs ||= {} + @raw_diffs[options] ||= load_diffs(st_diffs, options) end end -- cgit v1.2.3 From 443ae8c4e6682cd66eab0a2a7ec6ef913c0d684c Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Wed, 3 Aug 2016 16:45:06 -0700 Subject: Fix skip_repo parameter being ignored when destroying a namespace When destroying a namespace, the `skip_repo` parameter is supposed to prevent the repository directory from being destroyed and allow the namespace after_destroy hook to run. If the namespace fails to be deleted for some reason, we could be left with repositories that are deleted with existing projects. --- app/workers/project_destroy_worker.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/workers/project_destroy_worker.rb b/app/workers/project_destroy_worker.rb index b51c6a266c9..3062301a9b1 100644 --- a/app/workers/project_destroy_worker.rb +++ b/app/workers/project_destroy_worker.rb @@ -12,6 +12,6 @@ class ProjectDestroyWorker user = User.find(user_id) - ::Projects::DestroyService.new(project, user, params).execute + ::Projects::DestroyService.new(project, user, params.symbolize_keys).execute end end -- cgit v1.2.3 From 7612f1c4c6df200778f32098fbccf654a858894d Mon Sep 17 00:00:00 2001 From: Steve Halasz Date: Wed, 3 Aug 2016 23:11:35 -0400 Subject: Document that webhook secret token is sent in X-Gitlab-Token HTTP header --- app/views/shared/web_hooks/_form.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 2585ed9360b..470dac6d75b 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -19,7 +19,7 @@ = f.label :token, "Secret Token", class: 'label-light' = f.text_field :token, class: "form-control", placeholder: '' %p.help-block - Use this token to validate received payloads + Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. .form-group = f.label :url, "Trigger", class: 'label-light' %ul.list-unstyled -- cgit v1.2.3 From bb193801f892490d64cfe34563a77a7ae90ff5d8 Mon Sep 17 00:00:00 2001 From: Herminio Torres Date: Thu, 4 Aug 2016 01:04:01 -0300 Subject: Fix Mystery Guest Magic Variable - Change the name of the variable to assign the local variable for partial, rather than `i` use the `index`. Don't pass a local variable called `i` to a partial. --- app/views/projects/diffs/_diffs.html.haml | 2 +- app/views/projects/diffs/_file.html.haml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 20dc280c3b2..ebaf939f930 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -29,5 +29,5 @@ - next unless blob - blob.load_all_data!(diffs.project.repository) unless blob.only_display_raw? - = render 'projects/diffs/file', i: index, project: diffs.project, + = render 'projects/diffs/file', index: index, project: diffs.project, diff_file: diff_file, diff_commit: diff_commit, blob: blob diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index f914e13a1ec..f0a86fd6d40 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,6 +1,6 @@ -.diff-file.file-holder{id: "diff-#{i}", data: diff_file_html_data(project, diff_file)} +.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file)} .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"} - = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{i}" + = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}" - unless diff_file.submodule? .file-actions.hidden-xs -- cgit v1.2.3 From b6d545df51d7c3b4e572b40cdafb7efeb78edaf4 Mon Sep 17 00:00:00 2001 From: Tim Masliuchenko Date: Thu, 21 Jul 2016 21:44:12 +0300 Subject: Add unfold links for Side-by-Side view --- app/assets/javascripts/diff.js | 29 ++++++----------- app/controllers/projects/blob_controller.rb | 2 ++ app/helpers/diff_helper.rb | 39 +++++++++++++++-------- app/views/projects/blob/diff.html.haml | 34 +++++++++++++------- app/views/projects/diffs/_content.html.haml | 2 +- app/views/projects/diffs/_diffs.html.haml | 2 +- app/views/projects/diffs/_line.html.haml | 3 +- app/views/projects/diffs/_match_line.html.haml | 7 ---- app/views/projects/diffs/_parallel_view.html.haml | 10 +++--- app/views/projects/diffs/_text_file.html.haml | 5 ++- app/views/projects/merge_requests/_show.html.haml | 2 +- 11 files changed, 70 insertions(+), 65 deletions(-) delete mode 100644 app/views/projects/diffs/_match_line.html.haml (limited to 'app') diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 298f3852085..3dd7ceba92f 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -10,7 +10,7 @@ $(document).off('click', '.js-unfold'); $(document).on('click', '.js-unfold', (function(_this) { return function(event) { - var line_number, link, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; + var line_number, link, file, offset, old_line, params, prev_new_line, prev_old_line, ref, ref1, since, target, to, unfold, unfoldBottom; target = $(event.target); unfoldBottom = target.hasClass('js-unfold-bottom'); unfold = true; @@ -31,14 +31,16 @@ unfold = false; } } - link = target.parents('.diff-file').attr('data-blob-diff-path'); + file = target.parents('.diff-file'); + link = file.data('blob-diff-path'); params = { since: since, to: to, bottom: unfoldBottom, offset: offset, unfold: unfold, - indent: 1 + indent: 1, + view: file.data('view') }; return $.get(link, params, function(response) { return target.parent().replaceWith(response); @@ -48,26 +50,13 @@ } Diff.prototype.lineNumbers = function(line) { - var i, l, len, line_number, line_numbers, lines, results; if (!line.children().length) { return [0, 0]; } - lines = line.children().slice(0, 2); - line_numbers = (function() { - var i, len, results; - results = []; - for (i = 0, len = lines.length; i < len; i++) { - l = lines[i]; - results.push($(l).attr('data-linenumber')); - } - return results; - })(); - results = []; - for (i = 0, len = line_numbers.length; i < len; i++) { - line_number = line_numbers[i]; - results.push(parseInt(line_number)); - } - return results; + + return line.find('.diff-line-num').map(function() { + return parseInt($(this).data('linenumber')); + }); }; return Diff; diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index eda3727a28d..19d051720e9 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -76,6 +76,8 @@ class Projects::BlobController < Projects::ApplicationController end def diff + apply_diff_view_cookie! + @form = UnfoldForm.new(params) @lines = Gitlab::Highlight.highlight_lines(repository, @ref, @path) @lines = @lines[@form.since - 1..@form.to - 1] diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index cc7121b1163..f3c9ea074b4 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -13,12 +13,11 @@ module DiffHelper end def diff_view - diff_views = %w(inline parallel) - - if diff_views.include?(cookies[:diff_view]) - cookies[:diff_view] - else - diff_views.first + @diff_view ||= begin + diff_views = %w(inline parallel) + diff_view = cookies[:diff_view] + diff_view = diff_views.first unless diff_views.include?(diff_view) + diff_view.to_sym end end @@ -33,12 +32,23 @@ module DiffHelper options end - def unfold_bottom_class(bottom) - bottom ? 'js-unfold js-unfold-bottom' : '' - end + def diff_match_line(old_pos, new_pos, text: '', view: :inline, bottom: false) + content = content_tag :td, text, class: "line_content match #{view == :inline ? '' : view}" + cls = ['diff-line-num', 'unfold', 'js-unfold'] + cls << 'js-unfold-bottom' if bottom + + html = '' + if old_pos + html << content_tag(:td, '...', class: cls + ['old_line'], data: { linenumber: old_pos }) + html << content unless view == :inline + end + + if new_pos + html << content_tag(:td, '...', class: cls + ['new_line'], data: { linenumber: new_pos }) + html << content + end - def unfold_class(unfold) - unfold ? 'unfold js-unfold' : '' + html.html_safe end def diff_line_content(line, line_type = nil) @@ -67,11 +77,11 @@ module DiffHelper end def inline_diff_btn - diff_btn('Inline', 'inline', diff_view == 'inline') + diff_btn('Inline', 'inline', diff_view == :inline) end def parallel_diff_btn - diff_btn('Side-by-side', 'parallel', diff_view == 'parallel') + diff_btn('Side-by-side', 'parallel', diff_view == :parallel) end def submodule_link(blob, ref, repository = @repository) @@ -103,7 +113,8 @@ module DiffHelper commit = commit_for_diff(diff_file) { blob_diff_path: namespace_project_blob_diff_path(project.namespace, project, - tree_join(commit.id, diff_file.file_path)) + tree_join(commit.id, diff_file.file_path)), + view: diff_view } end diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 5926d181ba3..a79ae53c780 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -1,20 +1,30 @@ - if @lines.present? + - line_class = diff_view == :inline ? '' : diff_view - if @form.unfold? && @form.since != 1 && !@form.bottom? - %tr.line_holder - = render "projects/diffs/match_line", { line: @match_line, - line_old: @form.since, line_new: @form.since, bottom: false, new_file: false } + %tr.line_holder{ class: line_class } + = diff_match_line @form.since, @form.since, text: @match_line, view: diff_view - @lines.each_with_index do |line, index| - line_new = index + @form.since - line_old = line_new - @form.offset - %tr.line_holder{ id: line_old } - %td.old_line.diff-line-num{ data: { linenumber: line_old } } - = link_to raw(line_old), "##{line_old}" - %td.new_line.diff-line-num{ data: { linenumber: line_old } } - = link_to raw(line_new) , "##{line_old}" - %td.line_content.noteable_line==#{' ' * @form.indent}#{line} + - line_content = capture do + %td.line_content.noteable_line{ class: line_class }==#{' ' * @form.indent}#{line} + %tr.line_holder{ id: line_old, class: line_class } + - case diff_view + - when :inline + %td.old_line.diff-line-num{ data: { linenumber: line_old } } + %a{href: "##{line_old}", data: { linenumber: line_old }} + %td.new_line.diff-line-num{ data: { linenumber: line_new } } + %a{href: "##{line_new}", data: { linenumber: line_new }} + = line_content + - when :parallel + %td.old_line.diff-line-num{data: { linenumber: line_old }} + = link_to raw(line_old), "##{line_old}" + = line_content + %td.new_line.diff-line-num{data: { linenumber: line_new }} + = link_to raw(line_new), "##{line_new}" + = line_content - if @form.unfold? && @form.bottom? && @form.to < @blob.loc - %tr.line_holder{ id: @form.to } - = render "projects/diffs/match_line", { line: @match_line, - line_old: @form.to, line_new: @form.to, bottom: true, new_file: false } + %tr.line_holder{ id: @form.to, class: line_class } + = diff_match_line @form.to, @form.to, text: @match_line, view: diff_view, bottom: true diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml index a1b071f130c..d37961c4e40 100644 --- a/app/views/projects/diffs/_content.html.haml +++ b/app/views/projects/diffs/_content.html.haml @@ -13,7 +13,7 @@ .nothing-here-block.diff-collapsed{data: { diff_for_path: url } } This diff is collapsed. Click to expand it. - elsif diff_file.diff_lines.length > 0 - - if diff_view == 'parallel' + - if diff_view == :parallel = render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob - else = render "projects/diffs/text_file", diff_file: diff_file diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 20dc280c3b2..f707fa0ee5a 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,6 +1,6 @@ - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - diff_files = diffs.diff_files -- if diff_view == 'parallel' +- if diff_view == :parallel - fluid_layout true .content-block.oneline-block.files-changed diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml index 4d3af905b58..2d6a370b848 100644 --- a/app/views/projects/diffs/_line.html.haml +++ b/app/views/projects/diffs/_line.html.haml @@ -4,8 +4,7 @@ %tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } } - case type - when 'match' - = render "projects/diffs/match_line", { line: line.text, - line_old: line.old_pos, line_new: line.new_pos, bottom: false, new_file: diff_file.new_file } + = diff_match_line line.old_pos, line.new_pos, text: line.text - when 'nonewline' %td.old_line.diff-line-num %td.new_line.diff-line-num diff --git a/app/views/projects/diffs/_match_line.html.haml b/app/views/projects/diffs/_match_line.html.haml deleted file mode 100644 index d6dddd97879..00000000000 --- a/app/views/projects/diffs/_match_line.html.haml +++ /dev/null @@ -1,7 +0,0 @@ -%td.old_line.diff-line-num{data: {linenumber: line_old}, - class: [unfold_bottom_class(bottom), unfold_class(!new_file)]} - \... -%td.new_line.diff-line-num{data: {linenumber: line_new}, - class: [unfold_bottom_class(bottom), unfold_class(!new_file)]} - \... -%td.line_content.match= line diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index 7f30faa20d8..28aad3f4725 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,14 +1,15 @@ / Side-by-side diff view %div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data } %table + - last_line = 0 - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] + - last_line = right.new_pos if right %tr.line_holder.parallel - if left - if left.meta? - %td.old_line.diff-line-num.empty-cell - %td.line_content.parallel.match= left.text + = diff_match_line left.old_pos, nil, text: left.text, view: :parallel - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) @@ -21,8 +22,7 @@ - if right - if right.meta? - %td.old_line.diff-line-num.empty-cell - %td.line_content.parallel.match= left.text + = diff_match_line nil, right.new_pos, text: left.text, view: :parallel - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) @@ -37,3 +37,5 @@ - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) - if discussion_left || discussion_right = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right + - if !diff_file.new_file && last_line > 0 + = diff_match_line last_line, last_line, bottom: true, view: :parallel diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index 5970b9abf2b..ab5463ba89d 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -15,6 +15,5 @@ - if discussion = render "discussions/diff_discussion", discussion: discussion - - if last_line > 0 - = render "projects/diffs/match_line", { line: "", - line_old: last_line, line_new: last_line, bottom: true, new_file: diff_file.new_file } + - if !diff_file.new_file && last_line > 0 + = diff_match_line last_line, last_line, bottom: true diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 873ed9b59ee..269198adf91 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -2,7 +2,7 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes -- if diff_view == 'parallel' +- if diff_view == :parallel - fluid_layout true .merge-request{'data-url' => merge_request_path(@merge_request)} -- cgit v1.2.3 From c3ed78a4edf400e597803d91bb3afede4682afd7 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Thu, 4 Aug 2016 12:57:07 +0300 Subject: Refactor build artifacts documentation - Split user and admin documentation - Use new location paths - Add new Continuous Integration guide for the Admin area - Link to new guide from the Admin area --- app/views/admin/application_settings/_form.html.haml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 23b52d08df7..23f864df147 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -228,6 +228,9 @@ = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2' .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' + .help-block + Set the maximum file size each build's artifacts can have + = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") - if Gitlab.config.registry.enabled %fieldset @@ -385,4 +388,4 @@ .form-actions - = f.submit 'Save', class: 'btn btn-save' \ No newline at end of file + = f.submit 'Save', class: 'btn btn-save' -- cgit v1.2.3 From 6a0bbb5aa58e37a0ad8c3817c4e809143adce1be Mon Sep 17 00:00:00 2001 From: James Lopez Date: Tue, 2 Aug 2016 11:32:28 +0200 Subject: using shared path for project import uploads and refactored gitlab remove export worker --- .../import/gitlab_projects_controller.rb | 7 ++++--- app/models/project.rb | 5 ----- app/services/import_export_clean_up_service.rb | 24 ++++++++++++++++++++++ .../repository_archive_clean_up_service.rb | 4 ++-- app/workers/gitlab_remove_project_export_worker.rb | 9 -------- .../import_export_project_cleanup_worker.rb | 9 ++++++++ 6 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 app/services/import_export_clean_up_service.rb delete mode 100644 app/workers/gitlab_remove_project_export_worker.rb create mode 100644 app/workers/import_export_project_cleanup_worker.rb (limited to 'app') diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 30df1fb2fec..3ec173abcdb 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -12,13 +12,14 @@ class Import::GitlabProjectsController < Import::BaseController return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." }) end - imported_file = project_params[:file].path + "-import" + import_upload_path = Gitlab::ImportExport.import_upload_path(filename: project_params[:file].original_filename) - FileUtils.copy_entry(project_params[:file].path, imported_file) + FileUtils.mkdir_p(File.dirname(import_upload_path)) + FileUtils.copy_entry(project_params[:file].path, import_upload_path) @project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id], current_user, - File.expand_path(imported_file), + import_upload_path, project_params[:path]).execute if @project.saved? diff --git a/app/models/project.rb b/app/models/project.rb index 83b848ded8b..a18aef09acd 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -378,11 +378,6 @@ class Project < ActiveRecord::Base joins(join_body).reorder('join_note_counts.amount DESC') end - - # Deletes gitlab project export files older than 24 hours - def remove_gitlab_exports! - Gitlab::Popen.popen(%W(find #{Gitlab::ImportExport.storage_path} -not -path #{Gitlab::ImportExport.storage_path} -mmin +1440 -delete)) - end end def repository_storage_path diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb new file mode 100644 index 00000000000..6442406d77e --- /dev/null +++ b/app/services/import_export_clean_up_service.rb @@ -0,0 +1,24 @@ +class ImportExportCleanUpService + LAST_MODIFIED_TIME_IN_MINUTES = 1440 + + attr_reader :mmin, :path + + def initialize(mmin = LAST_MODIFIED_TIME_IN_MINUTES) + @mmin = mmin + @path = Gitlab::ImportExport.storage_path + end + + def execute + Gitlab::Metrics.measure(:import_export_clean_up) do + return unless File.directory?(path) + + clean_up_export_files + end + end + + private + + def clean_up_export_files + Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete)) + end +end diff --git a/app/services/repository_archive_clean_up_service.rb b/app/services/repository_archive_clean_up_service.rb index 0b56b09738d..aa84d36a206 100644 --- a/app/services/repository_archive_clean_up_service.rb +++ b/app/services/repository_archive_clean_up_service.rb @@ -1,6 +1,8 @@ class RepositoryArchiveCleanUpService LAST_MODIFIED_TIME_IN_MINUTES = 120 + attr_reader :mmin, :path + def initialize(mmin = LAST_MODIFIED_TIME_IN_MINUTES) @mmin = mmin @path = Gitlab.config.gitlab.repository_downloads_path @@ -17,8 +19,6 @@ class RepositoryArchiveCleanUpService private - attr_reader :mmin, :path - def clean_up_old_archives run(%W(find #{path} -not -path #{path} -type f \( -name \*.tar -o -name \*.bz2 -o -name \*.tar.gz -o -name \*.zip \) -maxdepth 2 -mmin +#{mmin} -delete)) end diff --git a/app/workers/gitlab_remove_project_export_worker.rb b/app/workers/gitlab_remove_project_export_worker.rb deleted file mode 100644 index 1d91897d520..00000000000 --- a/app/workers/gitlab_remove_project_export_worker.rb +++ /dev/null @@ -1,9 +0,0 @@ -class GitlabRemoveProjectExportWorker - include Sidekiq::Worker - - sidekiq_options queue: :default - - def perform - Project.remove_gitlab_exports! - end -end diff --git a/app/workers/import_export_project_cleanup_worker.rb b/app/workers/import_export_project_cleanup_worker.rb new file mode 100644 index 00000000000..72e3a9ae734 --- /dev/null +++ b/app/workers/import_export_project_cleanup_worker.rb @@ -0,0 +1,9 @@ +class ImportExportProjectCleanupWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform + ImportExportCleanUpService.new.execute + end +end -- cgit v1.2.3 From 4d716b765c66d51aa8026ce8622d704c2d52844a Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 4 Aug 2016 12:07:46 +0100 Subject: Underscore variable to camelCase --- app/assets/javascripts/gl_dropdown.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 356810c85cc..cc7e422fd89 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -53,9 +53,8 @@ if (this.options.remote) { clearTimeout(timeout); return timeout = setTimeout(function() { - var blur_field; - blur_field = this.shouldBlur(keyCode); - if (blur_field && this.filterInputBlur) { + var blurField = this.shouldBlur(keyCode); + if (blurField && this.filterInputBlur) { this.input.blur(); } return this.options.query(this.input.val(), function(data) { -- cgit v1.2.3 From f15ed5f0a5c298a2f0eb5aaa6d848364133532a5 Mon Sep 17 00:00:00 2001 From: Herminio Torres Date: Thu, 4 Aug 2016 01:35:17 -0300 Subject: Fix Rename `add_users_into_project` and `projects_ids` We never add things `into` projects, we just add them `to` projects. So how about we rename this to `add_users_to_project`. Rename `projects_ids` to `project_ids` by following the convention of rails. --- app/models/members/project_member.rb | 6 +++--- app/models/project_team.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index f39afc61ce9..f176feddbad 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -21,19 +21,19 @@ class ProjectMember < Member # or symbol like :master representing role # # Ex. - # add_users_into_projects( + # add_users_to_projects( # project_ids, # user_ids, # ProjectMember::MASTER # ) # - # add_users_into_projects( + # add_users_to_projects( # project_ids, # user_ids, # :master # ) # - def add_users_into_projects(project_ids, user_ids, access, current_user = nil) + def add_users_to_projects(project_ids, user_ids, access, current_user = nil) access_level = if roles_hash.has_key?(access) roles_hash[access] elsif roles_hash.values.include?(access.to_i) diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 19fd082534c..d0a714cd6fc 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -34,7 +34,7 @@ class ProjectTeam end def add_users(users, access, current_user = nil) - ProjectMember.add_users_into_projects( + ProjectMember.add_users_to_projects( [project.id], users, access, -- cgit v1.2.3 From b3a9ad6a57d34632cd011b242faad4584c25274f Mon Sep 17 00:00:00 2001 From: dixpac Date: Thu, 4 Aug 2016 11:53:23 +0200 Subject: Add description to text/plain emails Added to * new_issue_email * new_merge_request_email --- app/views/notify/new_issue_email.text.erb | 2 ++ app/views/notify/new_merge_request_email.text.erb | 2 ++ 2 files changed, 4 insertions(+) (limited to 'app') diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb index fc64c98038b..ca5c2f2688c 100644 --- a/app/views/notify/new_issue_email.text.erb +++ b/app/views/notify/new_issue_email.text.erb @@ -3,3 +3,5 @@ New Issue was created. Issue <%= @issue.iid %>: <%= url_for(namespace_project_issue_url(@issue.project.namespace, @issue.project, @issue)) %> Author: <%= @issue.author_name %> Assignee: <%= @issue.assignee_name %> + +<%= @issue.description %> diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index d4aad8d1862..3c8f178ac77 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -6,3 +6,5 @@ New Merge Request <%= @merge_request.to_reference %> Author: <%= @merge_request.author_name %> Assignee: <%= @merge_request.assignee_name %> +<%= @merge_request.description %> + -- cgit v1.2.3 From e9015b3541cbd0641a38ec0056e81591dda84688 Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 4 Aug 2016 16:00:18 +0300 Subject: Set consistency in list text height css Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/lists.scss | 11 ++++++++--- app/assets/stylesheets/framework/variables.scss | 2 ++ app/assets/stylesheets/pages/groups.scss | 8 +------- app/assets/stylesheets/pages/projects.scss | 8 +------- 4 files changed, 12 insertions(+), 17 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 2c40ec430ca..965fcc06518 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -114,6 +114,12 @@ ul.content-list { font-size: $list-font-size; color: $list-text-color; + &.no-description { + .title { + line-height: $list-text-height; + } + } + .title { font-weight: 600; } @@ -134,12 +140,11 @@ ul.content-list { } .controls { - padding-top: 1px; float: right; > .control-text { margin-right: $gl-padding-top; - line-height: 40px; + line-height: $list-text-height; &:last-child { margin-right: 0; @@ -150,7 +155,7 @@ ul.content-list { > .btn-group { margin-right: $gl-padding-top; display: inline-block; - margin-top: 4px; + margin-top: 3px; margin-bottom: 4px; &:last-child { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 1882d4e888d..06cd80bf3e4 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -43,6 +43,8 @@ $gl-header-color: $gl-title-color; $list-font-size: $gl-font-size; $list-title-color: $gl-title-color; $list-text-color: $gl-text-color; +$list-text-height: 42px; + /* * Markdown diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 2a3acc3eb4c..b657ca47d38 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -23,15 +23,9 @@ } .group-row { - &.no-description { - .group-name { - line-height: 44px; - } - } - .stats { float: right; - line-height: 44px; + line-height: $list-text-height; color: $gl-gray; span { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 4409477916f..d91c8e61165 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -512,18 +512,12 @@ pre.light-well { .project-row { border-color: $table-border-color; - &.no-description { - .project { - line-height: 40px; - } - } - .project-full-name { @include str-truncated; } .controls { - line-height: 40px; + line-height: $list-text-height; a:hover { text-decoration: none; -- cgit v1.2.3 From 4e0c62c69631e4553618bf05d42ae6f8d38684ac Mon Sep 17 00:00:00 2001 From: Dmitriy Zaporozhets Date: Thu, 4 Aug 2016 16:31:10 +0300 Subject: Remove unnecessary empty line after css var Signed-off-by: Dmitriy Zaporozhets --- app/assets/stylesheets/framework/variables.scss | 1 - 1 file changed, 1 deletion(-) (limited to 'app') diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 06cd80bf3e4..ca720022539 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -45,7 +45,6 @@ $list-title-color: $gl-title-color; $list-text-color: $gl-text-color; $list-text-height: 42px; - /* * Markdown */ -- cgit v1.2.3 From 705085db0c3b869f62f1b0f742686cc2082001fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 4 Aug 2016 16:00:31 +0200 Subject: Move abilities by subject class to a dedicated method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will avoid lame conflicts when merging CE to EE Signed-off-by: Rémy Coutable --- app/models/ability.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app') diff --git a/app/models/ability.rb b/app/models/ability.rb index d95a2507199..d9113ffd99a 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -6,6 +6,10 @@ class Ability return [] unless user.is_a?(User) return [] if user.blocked? + abilities_by_subject_class(user: user, subject: subject) + end + + def abilities_by_subject_class(user:, subject:) case subject when CommitStatus then commit_status_abilities(user, subject) when Project then project_abilities(user, subject) -- cgit v1.2.3 From 984367f957c8f8d02fa82b08817e2f2f318c6bff Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 4 Aug 2016 23:44:27 +0800 Subject: Move those builders to their own namespace, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5620#note_13540099 --- app/models/ci/build.rb | 4 ++-- app/models/project_services/builds_email_service.rb | 2 +- app/models/service.rb | 2 +- app/services/delete_branch_service.rb | 2 +- app/services/delete_tag_service.rb | 2 +- app/services/git_push_service.rb | 4 ++-- app/services/git_tag_push_service.rb | 4 ++-- app/services/notes/post_process_service.rb | 2 +- app/services/test_hook_service.rb | 3 ++- 9 files changed, 13 insertions(+), 12 deletions(-) (limited to 'app') diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 08f396210c9..b919846af22 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -349,7 +349,7 @@ module Ci def execute_hooks return unless project - build_data = Gitlab::BuildDataBuilder.build(self) + build_data = Gitlab::DataBuilder::BuildDataBuilder.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) project.running_or_pending_build_count(force: true) @@ -461,7 +461,7 @@ module Ci def build_attributes_from_config return {} unless pipeline.config_processor - + pipeline.config_processor.build_attributes(name) end end diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index 5e166471077..bf8c68244a1 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -52,7 +52,7 @@ class BuildsEmailService < Service def test_data(project = nil, user = nil) build = project.builds.last - Gitlab::BuildDataBuilder.build(build) + Gitlab::DataBuilder::BuildDataBuilder.build(build) end def fields diff --git a/app/models/service.rb b/app/models/service.rb index e4cd44f542a..76f588f234d 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -80,7 +80,7 @@ class Service < ActiveRecord::Base end def test_data(project, user) - Gitlab::PushDataBuilder.build_sample(project, user) + Gitlab::DataBuilder::PushDataBuilder.build_sample(project, user) end def event_channel_names diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 87f066edb6f..33c0fdc3c9d 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -39,7 +39,7 @@ class DeleteBranchService < BaseService end def build_push_data(branch) - Gitlab::PushDataBuilder + Gitlab::DataBuilder::PushDataBuilder .build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) end end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index 32e0eed6b63..41f8006b46c 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -33,7 +33,7 @@ class DeleteTagService < BaseService end def build_push_data(tag) - Gitlab::PushDataBuilder + Gitlab::DataBuilder::PushDataBuilder .build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 3f6a177bf3a..473eb5d902f 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -138,12 +138,12 @@ class GitPushService < BaseService end def build_push_data - @push_data ||= Gitlab::PushDataBuilder. + @push_data ||= Gitlab::DataBuilder::PushDataBuilder. build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits) end def build_push_data_system_hook - @push_data_system ||= Gitlab::PushDataBuilder. + @push_data_system ||= Gitlab::DataBuilder::PushDataBuilder. build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], []) end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 969530c4fdc..73bbbc36270 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -34,12 +34,12 @@ class GitTagPushService < BaseService end end - Gitlab::PushDataBuilder. + Gitlab::DataBuilder::PushDataBuilder. build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message) end def build_system_push_data - Gitlab::PushDataBuilder. + Gitlab::DataBuilder::PushDataBuilder. build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '') end end diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index 534c48aefff..a9ee7949936 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -16,7 +16,7 @@ module Notes end def hook_data - Gitlab::NoteDataBuilder.build(@note, @note.author) + Gitlab::DataBuilder::NoteDataBuilder.build(@note, @note.author) end def execute_note_hooks diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb index e85e58751e7..60b85882092 100644 --- a/app/services/test_hook_service.rb +++ b/app/services/test_hook_service.rb @@ -1,6 +1,7 @@ class TestHookService def execute(hook, current_user) - data = Gitlab::PushDataBuilder.build_sample(hook.project, current_user) + data = Gitlab::DataBuilder::PushDataBuilder. + build_sample(hook.project, current_user) hook.execute(data, 'push_hooks') end end -- cgit v1.2.3 From 3b2c5a85414090d93de33e26912b3ac2d771dfe9 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 5 Aug 2016 00:31:55 +0800 Subject: Touch it after builds were created, aligning with: CreateCommitBuildsService --- app/services/ci/create_pipeline_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 7a8b0683acb..b3772968ef3 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -27,13 +27,13 @@ module Ci end pipeline.save! - pipeline.touch unless pipeline.create_builds(current_user) pipeline.errors.add(:base, 'No builds for this pipeline.') end pipeline.save + pipeline.touch pipeline end -- cgit v1.2.3 From 584258dbb82f76c627d8552fc96689c7879b36f6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 5 Aug 2016 00:43:16 +0800 Subject: Share nothing so it's safest, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5620#note_13581090 --- app/models/ci/pipeline.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 81991e8aa60..59ab8b5ce35 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -252,9 +252,12 @@ module Ci end def execute_hooks - pipeline_data = Gitlab::DataBuilder::PipelineDataBuilder.build(self) project.execute_hooks(pipeline_data, :pipeline_hooks) - project.execute_services(pipeline_data.dup, :pipeline_hooks) + project.execute_services(pipeline_data, :pipeline_hooks) + end + + def pipeline_data + Gitlab::DataBuilder::PipelineDataBuilder.build(self) end def keep_around_commits -- cgit v1.2.3 From 901536b36f3f5d95bd9ba33a2b99ef1c171c1133 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 5 Aug 2016 00:53:07 +0800 Subject: No need to check that as in CreateCommitBuildsService: Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5620#note_13581358 --- app/models/ci/pipeline.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 59ab8b5ce35..d6b75411022 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -231,7 +231,7 @@ module Ci last_status = status if update_state_from_commit_statuses - execute_hooks if last_status != status && !skip_ci? + execute_hooks if last_status != status true else false -- cgit v1.2.3 From 5b5150301725fd192c328ee811facf2c89ffb528 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Thu, 4 Aug 2016 10:09:13 -0500 Subject: Fix filter label tooltip HTML rendering --- app/assets/stylesheets/pages/labels.scss | 11 +++++++++++ app/views/shared/_labels_row.html.haml | 6 +----- 2 files changed, 12 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 3b1e38fc07d..606459f82cd 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -182,6 +182,17 @@ .btn { color: inherit; } + + a.btn { + padding: 0; + + .has-tooltip { + top: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + line-height: 1.1; + } + } } .label-options-toggle { diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml index dce492352ac..e324d0e5203 100644 --- a/app/views/shared/_labels_row.html.haml +++ b/app/views/shared/_labels_row.html.haml @@ -1,9 +1,5 @@ - labels.each do |label| %span.label-row.btn-group{ role: "group", aria: { label: label.name }, style: "color: #{text_color_for_bg(label.color)}" } - = link_to label.name, label_filter_path(@project, label, type: controller.controller_name), - class: "btn btn-transparent has-tooltip", - style: "background-color: #{label.color};", - title: escape_once(label.description), - data: { container: "body" } + = link_to_label(label, css_class: 'btn btn-transparent') %button.btn.btn-transparent.label-remove.js-label-filter-remove{ type: "button", style: "background-color: #{label.color};", data: { label: label.title } } = icon("times") -- cgit v1.2.3 From 9955dc29a863ad997efe2926875c29f963ba94d4 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 12 Jul 2016 17:52:36 -0500 Subject: Update timeago to shorter representation --- app/assets/javascripts/application.js | 6 ++-- .../javascripts/lib/utils/datetime_utility.js | 38 +++++++++++++++++++++- app/helpers/application_helper.rb | 8 +++-- .../projects/ci/pipelines/_pipeline.html.haml | 2 +- 4 files changed, 48 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 127e568adc9..f1aab067351 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -287,7 +287,7 @@ $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned'); $('.navbar-fixed-top').removeClass('header-pinned-nav'); } - return $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) { + $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) { var $page, $pinBtn, $tooltip, $topNav, doPinNav, tooltipText; e.preventDefault(); $pinBtn = $(e.currentTarget); @@ -315,6 +315,8 @@ $tooltip.find('.tooltip-inner').text(tooltipText); return $pinBtn.attr('title', tooltipText).tooltip('fixTitle'); }); - }); + // Custom time ago + gl.utils.shortTimeAgo($('.js-short-timeago')); + }); }).call(this); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index e817261f210..10afa7e4329 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -8,13 +8,16 @@ base.utils = {}; } w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; + w.gl.utils.formatDate = function(datetime) { return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); }; + w.gl.utils.getDayName = function(date) { return this.days[date.getDay()]; }; - return w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) { + + w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) { if (setTimeago == null) { setTimeago = true; } @@ -31,6 +34,39 @@ }); } }; + + w.gl.utils.shortTimeAgo = function($el) { + var shortLocale, tmpLocale; + shortLocale = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: 'ago', + suffixFromNow: 'from now', + seconds: '1 min', + minute: '1 min', + minutes: '%d mins', + hour: '1 hr', + hours: '%d hrs', + day: '1 day', + days: '%d days', + month: '1 month', + months: '%d months', + year: '1 year', + years: '%d years', + wordSeparator: ' ', + numbers: [] + }; + tmpLocale = $.timeago.settings.strings; + $el.each(function(el) { + var $el1; + $el1 = $(this); + return $el1.attr('title', gl.utils.formatDate($el.attr('datetime'))); + }); + $.timeago.settings.strings = shortLocale; + $el.timeago(); + $.timeago.settings.strings = tmpLocale; + }; + })(window); }).call(this); diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 50de93d4bdf..c3613bc67dd 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -163,9 +163,13 @@ module ApplicationHelper # `html_class` argument is provided. # # Returns an HTML-safe String - def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false) + def time_ago_with_tooltip(time, placement: 'top', html_class: '', skip_js: false, short_format: false) + css_classes = short_format ? 'js-short-timeago' : 'js-timeago' + css_classes << " #{html_class}" unless html_class.blank? + css_classes << ' js-timeago-pending' unless skip_js + element = content_tag :time, time.to_s, - class: "#{html_class} js-timeago #{"js-timeago-pending" unless skip_js}", + class: css_classes, datetime: time.to_time.getutc.iso8601, title: time.to_time.in_time_zone.to_s(:medium), data: { toggle: 'tooltip', placement: placement, container: 'body' } diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 558c35553da..9a594877803 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -53,7 +53,7 @@ - if pipeline.finished_at %p.finished-at = icon("calendar") - #{time_ago_with_tooltip(pipeline.finished_at)} + #{time_ago_with_tooltip(pipeline.finished_at, short_format: true, skip_js: true)} %td.pipeline-actions .controls.hidden-xs.pull-right -- cgit v1.2.3 From 482d7802cc71280595cad71882bf1b438461e435 Mon Sep 17 00:00:00 2001 From: tiagonbotelho Date: Mon, 1 Aug 2016 16:48:15 +0100 Subject: changes default_branch_protection to allow devs_can_merge protection option aswell --- app/models/project.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) (limited to 'app') diff --git a/app/models/project.rb b/app/models/project.rb index 507813bccf8..16a418d5a3f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -876,14 +876,8 @@ class Project < ActiveRecord::Base ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present? end - def developers_can_push_to_protected_branch?(branch_name) - return true if empty_repo? && !default_branch_protected? - - protected_branches.matching(branch_name).any?(&:developers_can_push) - end - - def developers_can_merge_to_protected_branch?(branch_name) - protected_branches.matching(branch_name).any?(&:developers_can_merge) + def user_can_push_to_empty_repo?(user) + !default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER end def forked? @@ -1278,7 +1272,8 @@ class Project < ActiveRecord::Base private def default_branch_protected? - current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL + current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || + current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE end def authorized_for_user_by_group?(user, min_access_level) -- cgit v1.2.3 From 51cf14b37c64b1afab25b10dfe94e281ff58d288 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre Date: Tue, 2 Aug 2016 13:46:02 -0300 Subject: Does not need to disable GitHub webhooks since PRs are check out locally --- app/views/import/github/status.html.haml | 4 ---- 1 file changed, 4 deletions(-) (limited to 'app') diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml index deaaf9af875..54ff1d27c67 100644 --- a/app/views/import/github/status.html.haml +++ b/app/views/import/github/status.html.haml @@ -4,10 +4,6 @@ %i.fa.fa-github Import projects from GitHub -%p - %i.fa.fa-warning - To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository. - %p.light Select projects you want to import. %hr -- cgit v1.2.3 From 554e18ab025fcd86001faa57fab14fe3ca28a672 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Fri, 5 Aug 2016 11:35:44 +0200 Subject: Create service for enabling deploy keys --- app/controllers/projects/deploy_keys_controller.rb | 20 ++++++-------------- app/services/enable_deploy_key_service.rb | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 app/services/enable_deploy_key_service.rb (limited to 'app') diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 83d5ced9be8..ade2c54552b 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -12,8 +12,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def new - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) end def create @@ -21,19 +20,16 @@ class Projects::DeployKeysController < Projects::ApplicationController set_index_vars if @key.valid? && @project.deploy_keys << @key - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) else render "index" end end def enable - @key = accessible_keys.find(params[:id]) - @project.deploy_keys << @key + EnableDeployKeyService.new(@project, current_user, params).execute - redirect_to namespace_project_deploy_keys_path(@project.namespace, - @project) + redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) end def disable @@ -45,9 +41,9 @@ class Projects::DeployKeysController < Projects::ApplicationController protected def set_index_vars - @enabled_keys ||= @project.deploy_keys + @enabled_keys ||= @project.deploy_keys - @available_keys ||= accessible_keys - @enabled_keys + @available_keys ||= current_user.accessible_deploy_keys - @enabled_keys @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys @available_public_keys ||= DeployKey.are_public - @enabled_keys @@ -56,10 +52,6 @@ class Projects::DeployKeysController < Projects::ApplicationController @available_public_keys -= @available_project_keys end - def accessible_keys - @accessible_keys ||= current_user.accessible_deploy_keys - end - def deploy_key_params params.require(:deploy_key).permit(:key, :title) end diff --git a/app/services/enable_deploy_key_service.rb b/app/services/enable_deploy_key_service.rb new file mode 100644 index 00000000000..baa4a9dd2d4 --- /dev/null +++ b/app/services/enable_deploy_key_service.rb @@ -0,0 +1,14 @@ +class EnableDeployKeyService < BaseService + def execute + key = accessible_keys.find_by(id: params[:key_id] || params[:id]) + + project.deploy_keys << key if key + key + end + + private + + def accessible_keys + current_user.accessible_deploy_keys + end +end -- cgit v1.2.3 From c74005e75cf29eb14d2e9f5a2c3744b6e06ded0a Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Thu, 28 Jul 2016 15:12:49 +0200 Subject: Log base64-decoded PostReceive arguments The change to base64-encoding the third argument to PostReceive in gitlab-shell made our Sidekiq ArgumentsLogger a little less useful. This change adds a log statement for the decoded data. Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/20381 --- app/workers/post_receive.rb | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app') diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index 09035a7cf2d..a9a2b716005 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -10,6 +10,10 @@ class PostReceive log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"") end + changes = Base64.decode64(changes) unless changes.include?(' ') + # Use Sidekiq.logger so arguments can be correlated with execution + # time and thread ID's. + Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS'] post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes) if post_received.project.nil? -- cgit v1.2.3 From d3deba04f929f91082eaafabc405b4acef0d3d68 Mon Sep 17 00:00:00 2001 From: Rik de Groot Date: Mon, 1 Aug 2016 00:38:24 +0200 Subject: fix: respect data-attribute 'skip-users' in user_selects * Respect parameters of each individual select, instead of the global. * Update skipped users in approver search when removed from default approvers. --- app/assets/javascripts/users_select.js | 38 ++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 4af2a214e12..65d362e072c 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -13,14 +13,15 @@ } $('.js-user-search').each((function(_this) { return function(i, dropdown) { + var options = {}; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser; $dropdown = $(dropdown); - _this.projectId = $dropdown.data('project-id'); - _this.showCurrentUser = $dropdown.data('current-user'); + options.projectId = $dropdown.data('project-id'); + options.showCurrentUser = $dropdown.data('current-user'); showNullUser = $dropdown.data('null-user'); showAnyUser = $dropdown.data('any-user'); firstUser = $dropdown.data('first-user'); - _this.authorId = $dropdown.data('author-id'); + options.authorId = $dropdown.data('author-id'); selectedId = $dropdown.data('selected'); defaultLabel = $dropdown.data('default-label'); issueURL = $dropdown.data('issueUpdate'); @@ -75,7 +76,7 @@ data: function(term, callback) { var isAuthorFilter; isAuthorFilter = $('.js-author-search'); - return _this.users(term, function(users) { + return _this.users(term, options, function(users) { var anyUser, index, j, len, name, obj, showDivider; if (term.length === 0) { showDivider = 0; @@ -185,11 +186,14 @@ $('.ajax-users-select').each((function(_this) { return function(i, select) { var firstUser, showAnyUser, showEmailUser, showNullUser; - _this.projectId = $(select).data('project-id'); - _this.groupId = $(select).data('group-id'); - _this.showCurrentUser = $(select).data('current-user'); - _this.authorId = $(select).data('author-id'); - _this.skipUsers = $(select).data('skip-users'); + var options = {}; + options.skipLdap = $(select).hasClass('skip_ldap'); + options.projectId = $(select).data('project-id'); + options.groupId = $(select).data('group-id'); + options.showCurrentUser = $(select).data('current-user'); + options.pushCodeToProtectedBranches = $(select).data('push-code-to-protected-branches'); + options.authorId = $(select).data('author-id'); + options.skipUsers = $(select).data('skip-users'); showNullUser = $(select).data('null-user'); showAnyUser = $(select).data('any-user'); showEmailUser = $(select).data('email-user'); @@ -199,7 +203,7 @@ multiple: $(select).hasClass('multiselect'), minimumInputLength: 0, query: function(query) { - return _this.users(query.term, function(users) { + return _this.users(query.term, options, function(users) { var anyUser, data, emailUser, index, j, len, name, nullUser, obj, ref; data = { results: users @@ -309,7 +313,7 @@ }); }; - UsersSelect.prototype.users = function(query, callback) { + UsersSelect.prototype.users = function(query, options, callback) { var url; url = this.buildUrl(this.usersPath); return $.ajax({ @@ -318,11 +322,13 @@ search: query, per_page: 20, active: true, - project_id: this.projectId, - group_id: this.groupId, - current_user: this.showCurrentUser, - author_id: this.authorId, - skip_users: this.skipUsers + project_id: options.projectId || null, + group_id: options.groupId || null, + skip_ldap: options.skipLdap || null, + current_user: options.showCurrentUser || null, + push_code_to_protected_branches: options.pushCodeToProtectedBranches || null, + author_id: options.authorId || null, + skip_users: options.skipUsers || null }, dataType: "json" }).done(function(users) { -- cgit v1.2.3 From 1ac953dab437b3f2eaeca0ae39b80e40f8a09848 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Mon, 1 Aug 2016 16:08:36 -0500 Subject: Update layout and JS for create protected branch. Also updates protect branch list --- .../javascripts/allowed_to_merge_select.js.es6 | 0 app/assets/javascripts/dispatcher.js | 3 +- app/assets/javascripts/protect_branch.js.es6 | 84 +++++++++++++++++++++ app/assets/javascripts/protected_branch_select.js | 72 ------------------ .../javascripts/protected_branch_select.js.es6 | 88 ++++++++++++++++++++++ .../protected_branches_access_select.js.es6 | 63 ---------------- app/assets/stylesheets/framework/dropdowns.scss | 8 ++ app/assets/stylesheets/pages/projects.scss | 16 ++-- .../protected_branches/_branches_list.html.haml | 47 ++++++------ .../_create_protected_branch.html.haml | 34 +++++++++ .../protected_branches/_dropdown.html.haml | 9 +-- .../protected_branches/_protected_branch.html.haml | 6 +- .../projects/protected_branches/index.html.haml | 36 +-------- 13 files changed, 255 insertions(+), 211 deletions(-) create mode 100644 app/assets/javascripts/allowed_to_merge_select.js.es6 create mode 100644 app/assets/javascripts/protect_branch.js.es6 delete mode 100644 app/assets/javascripts/protected_branch_select.js create mode 100644 app/assets/javascripts/protected_branch_select.js.es6 delete mode 100644 app/assets/javascripts/protected_branches_access_select.js.es6 create mode 100644 app/views/projects/protected_branches/_create_protected_branch.html.haml (limited to 'app') diff --git a/app/assets/javascripts/allowed_to_merge_select.js.es6 b/app/assets/javascripts/allowed_to_merge_select.js.es6 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 9e6901962c6..ca4593d87a7 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -173,8 +173,7 @@ new Search(); break; case 'projects:protected_branches:index': - new ProtectedBranchesAccessSelect($(".new_protected_branch"), false, true); - new ProtectedBranchesAccessSelect($(".protected-branches-list"), true, false); + new CreateProtectedBranch(); break; } switch (path.first()) { diff --git a/app/assets/javascripts/protect_branch.js.es6 b/app/assets/javascripts/protect_branch.js.es6 new file mode 100644 index 00000000000..be987adc4a7 --- /dev/null +++ b/app/assets/javascripts/protect_branch.js.es6 @@ -0,0 +1,84 @@ +class ProtectedBranchesAccessDropdown { + constructor(options) { + const { $dropdown, data, onSelect } = options; + + $dropdown.glDropdown({ + data: data, + selectable: true, + fieldName: $dropdown.data('field-name'), + toggleLabel(item) { + return item.text; + }, + clicked(item, $el, e) { + e.preventDefault(); + onSelect(); + } + }); + } +} + +class AllowedToMergeDropdowns { + constructor (options) { + const { $dropdowns, onSelect } = options; + + $dropdowns.each((i, el) => { + new ProtectedBranchesAccessDropdown({ + $dropdown: $(el), + data: gon.merge_access_levels, + onSelect: onSelect + }); + }); + } +} + +class AllowedToPushSelects { + constructor (options) { + const { $dropdowns, onSelect } = options; + + $dropdowns.each((i, el) => { + new ProtectedBranchesAccessDropdown({ + $dropdown: $(el), + data: gon.push_access_levels, + onSelect: onSelect + }); + }); + } +} + +class CreateProtectedBranch { + constructor() { + this.$wrap = this.$form = $('#new_protected_branch'); + this.buildDropdowns(); + } + + buildDropdowns() { + // Allowed to Merge dropdowns + new AllowedToMergeDropdowns({ + $dropdowns: this.$wrap.find('.js-allowed-to-merge'), + onSelect: this.onSelect.bind(this) + }); + + // Allowed to Push dropdowns + new AllowedToPushSelects({ + $dropdowns: this.$wrap.find('.js-allowed-to-push'), + onSelect: this.onSelect.bind(this) + }); + + new ProtectedBranchSelects({ + $dropdowns: this.$wrap.find('.js-protected-branch-select'), + onSelect: this.onSelect.bind(this) + }); + } + + // This will run after clicked callback + onSelect() { + // Enable submit button + const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); + const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); + const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); + + if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ + this.$form.find('[type="submit"]').removeAttr('disabled'); + } + } +} diff --git a/app/assets/javascripts/protected_branch_select.js b/app/assets/javascripts/protected_branch_select.js deleted file mode 100644 index 3a47fc972dc..00000000000 --- a/app/assets/javascripts/protected_branch_select.js +++ /dev/null @@ -1,72 +0,0 @@ -(function() { - var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; - - this.ProtectedBranchSelect = (function() { - function ProtectedBranchSelect(currentProject) { - this.toggleCreateNewButton = bind(this.toggleCreateNewButton, this); - this.getProtectedBranches = bind(this.getProtectedBranches, this); - $('.dropdown-footer').hide(); - this.dropdown = $('.js-protected-branch-select').glDropdown({ - data: this.getProtectedBranches, - filterable: true, - remote: false, - search: { - fields: ['title'] - }, - selectable: true, - toggleLabel: function(selected) { - if (selected && 'id' in selected) { - return selected.title; - } else { - return 'Protected Branch'; - } - }, - fieldName: 'protected_branch[name]', - text: function(protected_branch) { - return _.escape(protected_branch.title); - }, - id: function(protected_branch) { - return _.escape(protected_branch.id); - }, - onFilter: this.toggleCreateNewButton, - clicked: function() { - return $('.protect-branch-btn').attr('disabled', false); - } - }); - $('.create-new-protected-branch').on('click', (function(_this) { - return function(event) { - _this.dropdown.data('glDropdown').remote.execute(); - return _this.dropdown.data('glDropdown').selectRowAtIndex(event, 0); - }; - })(this)); - } - - ProtectedBranchSelect.prototype.getProtectedBranches = function(term, callback) { - if (this.selectedBranch) { - return callback(gon.open_branches.concat(this.selectedBranch)); - } else { - return callback(gon.open_branches); - } - }; - - ProtectedBranchSelect.prototype.toggleCreateNewButton = function(branchName) { - this.selectedBranch = { - title: branchName, - id: branchName, - text: branchName - }; - if (branchName === '') { - $('.protected-branch-select-footer-list').addClass('hidden'); - return $('.dropdown-footer').hide(); - } else { - $('.create-new-protected-branch').text("Create Protected Branch: " + branchName); - $('.protected-branch-select-footer-list').removeClass('hidden'); - return $('.dropdown-footer').show(); - } - }; - - return ProtectedBranchSelect; - - })(); - -}).call(this); diff --git a/app/assets/javascripts/protected_branch_select.js.es6 b/app/assets/javascripts/protected_branch_select.js.es6 new file mode 100644 index 00000000000..9a259a473ae --- /dev/null +++ b/app/assets/javascripts/protected_branch_select.js.es6 @@ -0,0 +1,88 @@ +class ProtectedBranchSelect { + constructor(options) { + this.onSelect = options.onSelect; + this.$dropdown = options.$dropdown; + this.$dropdownContainer = this.$dropdown.parent(); + this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); + this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch'); + + this.buildDropdown(); + this.bindEvents(); + + // Hide footer + this.$dropdownFooter.addClass('hidden'); + } + + buildDropdown() { + this.$dropdown.glDropdown({ + data: this.getProtectedBranches.bind(this), + filterable: true, + remote: false, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel(selected) { + return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; + }, + fieldName: 'protected_branch[name]', + text(protected_branch) { + return _.escape(protected_branch.title); + }, + id(protected_branch) { + return _.escape(protected_branch.id); + }, + onFilter: this.toggleCreateNewButton.bind(this), + clicked: (item, $el, e) => { + e.preventDefault(); + this.onSelect(); + } + }); + } + + bindEvents() { + this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); + } + + onClickCreateWildcard(e) { + this.$dropdown.data('glDropdown').remote.execute(); + this.$dropdown.data('glDropdown').selectRowAtIndex(e, 0); + } + + getProtectedBranches(term, callback) { + if (this.selectedBranch) { + callback(gon.open_branches.concat(this.selectedBranch)); + } else { + callback(gon.open_branches); + } + } + + toggleCreateNewButton(branchName) { + this.selectedBranch = { + title: branchName, + id: branchName, + text: branchName + }; + + if (branchName) { + this.$dropdownContainer + .find('.create-new-protected-branch') + .html(`Create wildcard ${branchName}`); + } + + this.$dropdownFooter.toggleClass('hidden', !branchName); + } +} + +class ProtectedBranchSelects { + constructor(options) { + const { $dropdowns, onSelect } = options; + + $dropdowns.each((i, el) => { + new ProtectedBranchSelect({ + $dropdown: $(el), + onSelect: onSelect + }); + }); + } + } diff --git a/app/assets/javascripts/protected_branches_access_select.js.es6 b/app/assets/javascripts/protected_branches_access_select.js.es6 deleted file mode 100644 index e98312bbf37..00000000000 --- a/app/assets/javascripts/protected_branches_access_select.js.es6 +++ /dev/null @@ -1,63 +0,0 @@ -class ProtectedBranchesAccessSelect { - constructor(container, saveOnSelect, selectDefault) { - this.container = container; - this.saveOnSelect = saveOnSelect; - - this.container.find(".allowed-to-merge").each((i, element) => { - var fieldName = $(element).data('field-name'); - var dropdown = $(element).glDropdown({ - data: gon.merge_access_levels, - selectable: true, - fieldName: fieldName, - clicked: _.chain(this.onSelect).partial(element).bind(this).value() - }); - - if (selectDefault) { - dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0); - } - }); - - - this.container.find(".allowed-to-push").each((i, element) => { - var fieldName = $(element).data('field-name'); - var dropdown = $(element).glDropdown({ - data: gon.push_access_levels, - selectable: true, - fieldName: fieldName, - clicked: _.chain(this.onSelect).partial(element).bind(this).value() - }); - - if (selectDefault) { - dropdown.data('glDropdown').selectRowAtIndex(document.createEvent("Event"), 0); - } - }); - } - - onSelect(dropdown, selected, element, e) { - $(dropdown).find('.dropdown-toggle-text').text(selected.text); - if (this.saveOnSelect) { - return $.ajax({ - type: "POST", - url: $(dropdown).data('url'), - dataType: "json", - data: { - _method: 'PATCH', - id: $(dropdown).data('id'), - protected_branch: { - ["" + ($(dropdown).data('type')) + "_attributes"]: { - "access_level": selected.id - } - } - }, - success: function() { - var row; - row = $(e.target); - return row.closest('tr').effect('highlight'); - }, - error: function() { - return new Flash("Failed to update branch!", "alert"); - } - }); - } - } -} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index c54eb0d6479..e8eafa15899 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -72,6 +72,14 @@ &.large { width: 200px; } + + &.wide { + width: 100%; + + + .dropdown-select { + width: 100%; + } + } } .dropdown-menu, diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index d91c8e61165..63ac471b356 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -656,13 +656,9 @@ pre.light-well { } .new_protected_branch { - .dropdown { - display: inline; - margin-left: 15px; - } - label { - min-width: 120px; + margin-top: 6px; + font-weight: normal; } } @@ -678,6 +674,14 @@ pre.light-well { font-weight: 600; } } + + &.table-bordered { + border-radius: 1px; + + th:not(:last-child), td:not(:last-child) { + border-right: solid 1px transparent; + } + } } .custom-notifications-form { diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 0603a014008..dc471f515d4 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,26 +1,25 @@ -%h5.prepend-top-0 - Already Protected (#{@protected_branches.size}) -- if @protected_branches.empty? - %p.settings-message.text-center - No branches are protected, protect a branch with the form above. -- else - - can_admin_project = can?(current_user, :admin_project, @project) +.panel.panel-default + - if @protected_branches.empty? + %p.settings-message.text-center + No branches are protected, protect a branch with the form above. + - else + - can_admin_project = can?(current_user, :admin_project, @project) - %table.table.protected-branches-list - %colgroup - %col{ width: "20%" } - %col{ width: "30%" } - %col{ width: "25%" } - %col{ width: "25%" } - %thead - %tr - %th Branch - %th Last commit - %th Allowed to merge - %th Allowed to push - - if can_admin_project - %th - %tbody - = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } + %table.table.protected-branches-list.table-bordered + %colgroup + %col{ width: "20%" } + %col{ width: "30%" } + %col{ width: "25%" } + %col{ width: "25%" } + %thead + %tr + %th Protected branch (#{@protected_branches.size}) + %th Last commit + %th Allowed to merge + %th Allowed to push + - if can_admin_project + %th + %tbody + = render partial: @protected_branches, locals: { can_admin_project: can_admin_project } - = paginate @protected_branches, theme: 'gitlab' + = paginate @protected_branches, theme: 'gitlab' diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml new file mode 100644 index 00000000000..7006b340b34 --- /dev/null +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -0,0 +1,34 @@ += form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| + .panel.panel-default + .panel-heading + %b Protect a branch + .panel-body + .form-horizontal + .form-group + %label.col-md-2.text-right + Branch: + .col-md-10 + = render partial: "dropdown", locals: { f: f } + .help-block + Wildcards such as + %code *-stable + or + %code production/* + are supported + .form-group + %label.col-md-2.text-right + Allowed to merge: + .col-md-10 + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-merge wide', + data: { field_name: 'protected_branch[merge_access_level_attributes][access_level]' }}) + .form-group + %label.col-md-2.text-right + Allowed to push: + .col-md-10 + = dropdown_tag('Select', + options: { toggle_class: 'js-allowed-to-push wide', + data: { field_name: 'protected_branch[push_access_level_attributes][access_level]' }}) + + .panel-footer + = f.submit 'Protect', class: 'btn-create btn', disabled: true diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml index b803d932e67..1281e6664dd 100644 --- a/app/views/projects/protected_branches/_dropdown.html.haml +++ b/app/views/projects/protected_branches/_dropdown.html.haml @@ -1,17 +1,14 @@ = f.hidden_field(:name) -= dropdown_tag("Protected Branch", - options: { title: "Pick protected branch", toggle_class: 'js-protected-branch-select js-filter-submit', += dropdown_tag('Select branch or create wildcard', + options: { toggle_class: 'js-protected-branch-select js-filter-submit wide', filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected branches", footer_content: true, data: { show_no: true, show_any: true, show_upcoming: true, selected: params[:protected_branch_name], project_id: @project.try(:id) } }) do - %ul.dropdown-footer-list.hidden.protected-branch-select-footer-list + %ul.dropdown-footer-list %li = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do Create new - -:javascript - new ProtectedBranchSelect(); diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 498e412235e..986d591b764 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -17,13 +17,13 @@ %td = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level = dropdown_tag(protected_branch.merge_access_level.humanize, - options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', + options: { toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "merge_access_level" }}) %td = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level = dropdown_tag(protected_branch.push_access_level.humanize, - options: { title: "Allowed to push", toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', + options: { toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "push_access_level" }}) - if can_admin_project %td - = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning btn-sm pull-right" + = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning' diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 4efe44c7233..49dcc9a6ba4 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -14,41 +14,7 @@ %li prevent anyone from deleting the branch %p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}. .col-lg-9 - %h5.prepend-top-0 - Protect a branch - if can? current_user, :admin_project, @project - = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| - = form_errors(@protected_branch) + = render 'create_protected_branch' - .form-group - = f.label :name, "Branch", class: "label-light" - = render partial: "dropdown", locals: { f: f } - %p.help-block - = link_to "Wildcards", help_page_path('user/project/protected_branches', anchor: "wildcard-protected-branches") - such as - %code *-stable - or - %code production/* - are supported. - - .form-group - = hidden_field_tag 'protected_branch[merge_access_level_attributes][access_level]' - = label_tag "Allowed to merge: ", nil, class: "label-light append-bottom-0" - = dropdown_tag("", - options: { title: "Allowed to merge", toggle_class: 'allowed-to-merge', - dropdown_class: 'dropdown-menu-selectable', - data: { field_name: "protected_branch[merge_access_level_attributes][access_level]" }}) - - .form-group - = hidden_field_tag 'protected_branch[push_access_level_attributes][access_level]' - = label_tag "Allowed to push: ", nil, class: "label-light append-bottom-0" - = dropdown_tag("", - options: { title: "Allowed to push", toggle_class: 'allowed-to-push', - dropdown_class: 'dropdown-menu-selectable', - data: { field_name: "protected_branch[push_access_level_attributes][access_level]" }}) - - - = f.submit "Protect", class: "btn-create btn protect-branch-btn", disabled: true - - %hr = render "branches_list" -- cgit v1.2.3 From 2b2c42a1fa5b052a6b8f0d4c43fd41e5df7b99c1 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 3 Aug 2016 13:02:42 -0500 Subject: Refactor of Protected Branch Edit List --- app/assets/javascripts/dispatcher.js | 3 +- app/assets/javascripts/protect_branch.js.es6 | 84 ---------------------- .../javascripts/protect_branch_create.js.es6 | 84 ++++++++++++++++++++++ app/assets/javascripts/protect_branch_edit.js.es6 | 69 ++++++++++++++++++ .../protected_branches/_protected_branch.html.haml | 15 ++-- 5 files changed, 162 insertions(+), 93 deletions(-) delete mode 100644 app/assets/javascripts/protect_branch.js.es6 create mode 100644 app/assets/javascripts/protect_branch_create.js.es6 create mode 100644 app/assets/javascripts/protect_branch_edit.js.es6 (limited to 'app') diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index ca4593d87a7..6a153978cf2 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -173,7 +173,8 @@ new Search(); break; case 'projects:protected_branches:index': - new CreateProtectedBranch(); + new ProtectedBranchCreate(); + new ProtectedBranchEditList(); break; } switch (path.first()) { diff --git a/app/assets/javascripts/protect_branch.js.es6 b/app/assets/javascripts/protect_branch.js.es6 deleted file mode 100644 index be987adc4a7..00000000000 --- a/app/assets/javascripts/protect_branch.js.es6 +++ /dev/null @@ -1,84 +0,0 @@ -class ProtectedBranchesAccessDropdown { - constructor(options) { - const { $dropdown, data, onSelect } = options; - - $dropdown.glDropdown({ - data: data, - selectable: true, - fieldName: $dropdown.data('field-name'), - toggleLabel(item) { - return item.text; - }, - clicked(item, $el, e) { - e.preventDefault(); - onSelect(); - } - }); - } -} - -class AllowedToMergeDropdowns { - constructor (options) { - const { $dropdowns, onSelect } = options; - - $dropdowns.each((i, el) => { - new ProtectedBranchesAccessDropdown({ - $dropdown: $(el), - data: gon.merge_access_levels, - onSelect: onSelect - }); - }); - } -} - -class AllowedToPushSelects { - constructor (options) { - const { $dropdowns, onSelect } = options; - - $dropdowns.each((i, el) => { - new ProtectedBranchesAccessDropdown({ - $dropdown: $(el), - data: gon.push_access_levels, - onSelect: onSelect - }); - }); - } -} - -class CreateProtectedBranch { - constructor() { - this.$wrap = this.$form = $('#new_protected_branch'); - this.buildDropdowns(); - } - - buildDropdowns() { - // Allowed to Merge dropdowns - new AllowedToMergeDropdowns({ - $dropdowns: this.$wrap.find('.js-allowed-to-merge'), - onSelect: this.onSelect.bind(this) - }); - - // Allowed to Push dropdowns - new AllowedToPushSelects({ - $dropdowns: this.$wrap.find('.js-allowed-to-push'), - onSelect: this.onSelect.bind(this) - }); - - new ProtectedBranchSelects({ - $dropdowns: this.$wrap.find('.js-protected-branch-select'), - onSelect: this.onSelect.bind(this) - }); - } - - // This will run after clicked callback - onSelect() { - // Enable submit button - const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); - const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); - - if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ - this.$form.find('[type="submit"]').removeAttr('disabled'); - } - } -} diff --git a/app/assets/javascripts/protect_branch_create.js.es6 b/app/assets/javascripts/protect_branch_create.js.es6 new file mode 100644 index 00000000000..830cc0beb73 --- /dev/null +++ b/app/assets/javascripts/protect_branch_create.js.es6 @@ -0,0 +1,84 @@ +class ProtectedBranchesAccessDropdown { + constructor(options) { + const { $dropdown, data, onSelect } = options; + + $dropdown.glDropdown({ + data: data, + selectable: true, + fieldName: $dropdown.data('field-name'), + toggleLabel(item) { + return item.text; + }, + clicked(item, $el, e) { + e.preventDefault(); + onSelect(); + } + }); + } +} + +class AllowedToMergeDropdowns { + constructor (options) { + const { $dropdowns, onSelect } = options; + + $dropdowns.each((i, el) => { + new ProtectedBranchesAccessDropdown({ + $dropdown: $(el), + data: gon.merge_access_levels, + onSelect: onSelect + }); + }); + } +} + +class AllowedToPushSelects { + constructor (options) { + const { $dropdowns, onSelect } = options; + + $dropdowns.each((i, el) => { + new ProtectedBranchesAccessDropdown({ + $dropdown: $(el), + data: gon.push_access_levels, + onSelect: onSelect + }); + }); + } +} + +class ProtectedBranchCreate { + constructor() { + this.$wrap = this.$form = $('#new_protected_branch'); + this.buildDropdowns(); + } + + buildDropdowns() { + // Allowed to Merge dropdowns + new AllowedToMergeDropdowns({ + $dropdowns: this.$wrap.find('.js-allowed-to-merge'), + onSelect: this.onSelect.bind(this) + }); + + // Allowed to Push dropdowns + new AllowedToPushSelects({ + $dropdowns: this.$wrap.find('.js-allowed-to-push'), + onSelect: this.onSelect.bind(this) + }); + + new ProtectedBranchSelects({ + $dropdowns: this.$wrap.find('.js-protected-branch-select'), + onSelect: this.onSelect.bind(this) + }); + } + + // This will run after clicked callback + onSelect() { + // Enable submit button + const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); + const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); + const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); + + if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ + this.$form.find('[type="submit"]').removeAttr('disabled'); + } + } +} diff --git a/app/assets/javascripts/protect_branch_edit.js.es6 b/app/assets/javascripts/protect_branch_edit.js.es6 new file mode 100644 index 00000000000..b8ba22a1d6c --- /dev/null +++ b/app/assets/javascripts/protect_branch_edit.js.es6 @@ -0,0 +1,69 @@ +class ProtectedBranchEdit { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + this.buildDropdowns(); + } + + buildDropdowns() { + + // Allowed to merge dropdown + new ProtectedBranchesAccessDropdown({ + $dropdown: this.$allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelect.bind(this) + }); + + // Allowed to push dropdown + new ProtectedBranchesAccessDropdown({ + $dropdown: this.$allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelect.bind(this) + }); + } + + onSelect() { + const $allowedToMergeInput = $(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); + const $allowedToPushInput = $(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + id: this.$wrap.data('banchId'), + protected_branch: { + merge_access_level_attributes: { + access_level: $allowedToMergeInput.val() + }, + push_access_level_attributes: { + access_level: $allowedToPushInput.val() + } + } + }, + success: () => { + this.$wrap.effect('highlight'); + }, + error: function() { + $.scrollTo(0); + new Flash('Failed to update branch!'); + } + }); + } +} + +class ProtectedBranchEditList { + constructor() { + this.$wrap = $('.protected-branches-list'); + + // Build edit forms + this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { + new ProtectedBranchEdit({ + $wrap: $(el) + }); + }); + } +} diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 986d591b764..5af4052de2f 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -1,5 +1,4 @@ -- url = namespace_project_protected_branch_path(@project.namespace, @project, protected_branch) -%tr +%tr.js-protected-branch-edit-form{ data: { url: namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), branch_id: protected_branch.id } } %td = protected_branch.name - if @project.root_ref?(protected_branch.name) @@ -16,14 +15,14 @@ (branch was removed from repository) %td = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level - = dropdown_tag(protected_branch.merge_access_level.humanize, - options: { toggle_class: 'allowed-to-merge', dropdown_class: 'dropdown-menu-selectable merge', - data: { field_name: "allowed_to_merge_#{protected_branch.id}", url: url, id: protected_branch.id, type: "merge_access_level" }}) + = dropdown_tag( (protected_branch.merge_access_level.humanize || 'Select') , + options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable', + data: { field_name: "allowed_to_merge_#{protected_branch.id}" }}) %td = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level - = dropdown_tag(protected_branch.push_access_level.humanize, - options: { toggle_class: 'allowed-to-push', dropdown_class: 'dropdown-menu-selectable push', - data: { field_name: "allowed_to_push_#{protected_branch.id}", url: url, id: protected_branch.id, type: "push_access_level" }}) + = dropdown_tag( (protected_branch.push_access_level.humanize || 'Select') , + options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable', + data: { field_name: "allowed_to_push_#{protected_branch.id}" }}) - if can_admin_project %td = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning' -- cgit v1.2.3 From bc5bc7cb9efe760a540b761eb3cd712ef3de129f Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 3 Aug 2016 16:45:38 -0500 Subject: Fix class names and move each class to its own file --- .../javascripts/allowed_to_merge_dropdowns.js.es6 | 13 ++++ .../javascripts/allowed_to_merge_select.js.es6 | 0 .../javascripts/allowed_to_push_dropdowns.js.es6 | 13 ++++ .../javascripts/protect_branch_create.js.es6 | 84 ---------------------- app/assets/javascripts/protect_branch_edit.js.es6 | 69 ------------------ .../protected_branch_access_dropdown.js.es6 | 18 +++++ .../javascripts/protected_branch_create.js.es6 | 37 ++++++++++ .../javascripts/protected_branch_edit.js.es6 | 56 +++++++++++++++ .../javascripts/protected_branch_edit_list.js.es6 | 12 ++++ .../javascripts/protected_branch_select.js.es6 | 6 +- app/assets/stylesheets/pages/projects.scss | 9 ++- .../protected_branches/_branches_list.html.haml | 12 ++-- 12 files changed, 167 insertions(+), 162 deletions(-) create mode 100644 app/assets/javascripts/allowed_to_merge_dropdowns.js.es6 delete mode 100644 app/assets/javascripts/allowed_to_merge_select.js.es6 create mode 100644 app/assets/javascripts/allowed_to_push_dropdowns.js.es6 delete mode 100644 app/assets/javascripts/protect_branch_create.js.es6 delete mode 100644 app/assets/javascripts/protect_branch_edit.js.es6 create mode 100644 app/assets/javascripts/protected_branch_access_dropdown.js.es6 create mode 100644 app/assets/javascripts/protected_branch_create.js.es6 create mode 100644 app/assets/javascripts/protected_branch_edit.js.es6 create mode 100644 app/assets/javascripts/protected_branch_edit_list.js.es6 (limited to 'app') diff --git a/app/assets/javascripts/allowed_to_merge_dropdowns.js.es6 b/app/assets/javascripts/allowed_to_merge_dropdowns.js.es6 new file mode 100644 index 00000000000..3f62ad26f0f --- /dev/null +++ b/app/assets/javascripts/allowed_to_merge_dropdowns.js.es6 @@ -0,0 +1,13 @@ +class AllowedToMergeDropdowns { + constructor (options) { + const { $dropdowns, onSelect } = options; + + $dropdowns.each((i, el) => { + new ProtectedBranchAccessDropdown({ + $dropdown: $(el), + data: gon.merge_access_levels, + onSelect: onSelect + }); + }); + } +} diff --git a/app/assets/javascripts/allowed_to_merge_select.js.es6 b/app/assets/javascripts/allowed_to_merge_select.js.es6 deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/app/assets/javascripts/allowed_to_push_dropdowns.js.es6 b/app/assets/javascripts/allowed_to_push_dropdowns.js.es6 new file mode 100644 index 00000000000..055be38ccdb --- /dev/null +++ b/app/assets/javascripts/allowed_to_push_dropdowns.js.es6 @@ -0,0 +1,13 @@ +class AllowedToPushDropdowns { + constructor (options) { + const { $dropdowns, onSelect } = options; + + $dropdowns.each((i, el) => { + new ProtectedBranchAccessDropdown({ + $dropdown: $(el), + data: gon.push_access_levels, + onSelect: onSelect + }); + }); + } +} \ No newline at end of file diff --git a/app/assets/javascripts/protect_branch_create.js.es6 b/app/assets/javascripts/protect_branch_create.js.es6 deleted file mode 100644 index 830cc0beb73..00000000000 --- a/app/assets/javascripts/protect_branch_create.js.es6 +++ /dev/null @@ -1,84 +0,0 @@ -class ProtectedBranchesAccessDropdown { - constructor(options) { - const { $dropdown, data, onSelect } = options; - - $dropdown.glDropdown({ - data: data, - selectable: true, - fieldName: $dropdown.data('field-name'), - toggleLabel(item) { - return item.text; - }, - clicked(item, $el, e) { - e.preventDefault(); - onSelect(); - } - }); - } -} - -class AllowedToMergeDropdowns { - constructor (options) { - const { $dropdowns, onSelect } = options; - - $dropdowns.each((i, el) => { - new ProtectedBranchesAccessDropdown({ - $dropdown: $(el), - data: gon.merge_access_levels, - onSelect: onSelect - }); - }); - } -} - -class AllowedToPushSelects { - constructor (options) { - const { $dropdowns, onSelect } = options; - - $dropdowns.each((i, el) => { - new ProtectedBranchesAccessDropdown({ - $dropdown: $(el), - data: gon.push_access_levels, - onSelect: onSelect - }); - }); - } -} - -class ProtectedBranchCreate { - constructor() { - this.$wrap = this.$form = $('#new_protected_branch'); - this.buildDropdowns(); - } - - buildDropdowns() { - // Allowed to Merge dropdowns - new AllowedToMergeDropdowns({ - $dropdowns: this.$wrap.find('.js-allowed-to-merge'), - onSelect: this.onSelect.bind(this) - }); - - // Allowed to Push dropdowns - new AllowedToPushSelects({ - $dropdowns: this.$wrap.find('.js-allowed-to-push'), - onSelect: this.onSelect.bind(this) - }); - - new ProtectedBranchSelects({ - $dropdowns: this.$wrap.find('.js-protected-branch-select'), - onSelect: this.onSelect.bind(this) - }); - } - - // This will run after clicked callback - onSelect() { - // Enable submit button - const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); - const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); - - if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ - this.$form.find('[type="submit"]').removeAttr('disabled'); - } - } -} diff --git a/app/assets/javascripts/protect_branch_edit.js.es6 b/app/assets/javascripts/protect_branch_edit.js.es6 deleted file mode 100644 index b8ba22a1d6c..00000000000 --- a/app/assets/javascripts/protect_branch_edit.js.es6 +++ /dev/null @@ -1,69 +0,0 @@ -class ProtectedBranchEdit { - constructor(options) { - this.$wrap = options.$wrap; - this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); - this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - - this.buildDropdowns(); - } - - buildDropdowns() { - - // Allowed to merge dropdown - new ProtectedBranchesAccessDropdown({ - $dropdown: this.$allowedToMergeDropdown, - data: gon.merge_access_levels, - onSelect: this.onSelect.bind(this) - }); - - // Allowed to push dropdown - new ProtectedBranchesAccessDropdown({ - $dropdown: this.$allowedToPushDropdown, - data: gon.push_access_levels, - onSelect: this.onSelect.bind(this) - }); - } - - onSelect() { - const $allowedToMergeInput = $(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); - const $allowedToPushInput = $(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); - - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - id: this.$wrap.data('banchId'), - protected_branch: { - merge_access_level_attributes: { - access_level: $allowedToMergeInput.val() - }, - push_access_level_attributes: { - access_level: $allowedToPushInput.val() - } - } - }, - success: () => { - this.$wrap.effect('highlight'); - }, - error: function() { - $.scrollTo(0); - new Flash('Failed to update branch!'); - } - }); - } -} - -class ProtectedBranchEditList { - constructor() { - this.$wrap = $('.protected-branches-list'); - - // Build edit forms - this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { - new ProtectedBranchEdit({ - $wrap: $(el) - }); - }); - } -} diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 new file mode 100644 index 00000000000..045c12570bd --- /dev/null +++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 @@ -0,0 +1,18 @@ +class ProtectedBranchAccessDropdown { + constructor(options) { + const { $dropdown, data, onSelect } = options; + + $dropdown.glDropdown({ + data: data, + selectable: true, + fieldName: $dropdown.data('field-name'), + toggleLabel(item) { + return item.text; + }, + clicked(item, $el, e) { + e.preventDefault(); + onSelect(); + } + }); + } +} diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 new file mode 100644 index 00000000000..efe91478b66 --- /dev/null +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -0,0 +1,37 @@ +class ProtectedBranchCreate { + constructor() { + this.$wrap = this.$form = $('#new_protected_branch'); + this.buildDropdowns(); + } + + buildDropdowns() { + // Allowed to Merge dropdowns + new AllowedToMergeDropdowns({ + $dropdowns: this.$wrap.find('.js-allowed-to-merge'), + onSelect: this.onSelect.bind(this) + }); + + // Allowed to Push dropdowns + new AllowedToPushDropdowns({ + $dropdowns: this.$wrap.find('.js-allowed-to-push'), + onSelect: this.onSelect.bind(this) + }); + + new ProtectedBranchDropdowns({ + $dropdowns: this.$wrap.find('.js-protected-branch-select'), + onSelect: this.onSelect.bind(this) + }); + } + + // This will run after clicked callback + onSelect() { + // Enable submit button + const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); + const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); + const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); + + if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ + this.$form.find('[type="submit"]').removeAttr('disabled'); + } + } +} diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6 new file mode 100644 index 00000000000..20f13427c0b --- /dev/null +++ b/app/assets/javascripts/protected_branch_edit.js.es6 @@ -0,0 +1,56 @@ +class ProtectedBranchEdit { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + this.buildDropdowns(); + } + + buildDropdowns() { + + // Allowed to merge dropdown + new ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelect.bind(this) + }); + + // Allowed to push dropdown + new ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelect.bind(this) + }); + } + + onSelect() { + const $allowedToMergeInput = $(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); + const $allowedToPushInput = $(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + id: this.$wrap.data('banchId'), + protected_branch: { + merge_access_level_attributes: { + access_level: $allowedToMergeInput.val() + }, + push_access_level_attributes: { + access_level: $allowedToPushInput.val() + } + } + }, + success: () => { + this.$wrap.effect('highlight'); + }, + error() { + $.scrollTo(0); + new Flash('Failed to update branch!'); + } + }); + } +} diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branch_edit_list.js.es6 new file mode 100644 index 00000000000..6021ed87e2a --- /dev/null +++ b/app/assets/javascripts/protected_branch_edit_list.js.es6 @@ -0,0 +1,12 @@ +class ProtectedBranchEditList { + constructor() { + this.$wrap = $('.protected-branches-list'); + + // Build edit forms + this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { + new ProtectedBranchEdit({ + $wrap: $(el) + }); + }); + } +} diff --git a/app/assets/javascripts/protected_branch_select.js.es6 b/app/assets/javascripts/protected_branch_select.js.es6 index 9a259a473ae..6ca90453d49 100644 --- a/app/assets/javascripts/protected_branch_select.js.es6 +++ b/app/assets/javascripts/protected_branch_select.js.es6 @@ -1,4 +1,4 @@ -class ProtectedBranchSelect { +class ProtectedBranchDropdown { constructor(options) { this.onSelect = options.onSelect; this.$dropdown = options.$dropdown; @@ -74,12 +74,12 @@ class ProtectedBranchSelect { } } -class ProtectedBranchSelects { +class ProtectedBranchDropdowns { constructor(options) { const { $dropdowns, onSelect } = options; $dropdowns.each((i, el) => { - new ProtectedBranchSelect({ + new ProtectedBranchDropdown({ $dropdown: $(el), onSelect: onSelect }); diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 63ac471b356..cf9aa02600d 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -675,7 +675,14 @@ pre.light-well { } } - &.table-bordered { + .settings-message { + margin: 0; + border-radius: 0 0 1px 1px; + padding: 20px 0; + border: none; + } + + .table-bordered { border-radius: 1px; th:not(:last-child), td:not(:last-child) { diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index dc471f515d4..777e96c073f 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,16 +1,18 @@ -.panel.panel-default +.panel.panel-default.protected-branches-list - if @protected_branches.empty? + .panel-heading + %b Protected branch (#{@protected_branches.size}) %p.settings-message.text-center - No branches are protected, protect a branch with the form above. + There are currently no protected braches, protect a branch with the form above. - else - can_admin_project = can?(current_user, :admin_project, @project) - %table.table.protected-branches-list.table-bordered + %table.table.table-bordered %colgroup - %col{ width: "20%" } - %col{ width: "30%" } %col{ width: "25%" } + %col{ width: "30%" } %col{ width: "25%" } + %col{ width: "20%" } %thead %tr %th Protected branch (#{@protected_branches.size}) -- cgit v1.2.3 From 3b228db1c20ed582c911e3269551fcdfd9f1bde7 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 3 Aug 2016 19:28:05 -0500 Subject: Remove unnecesary classes to generate dropdowns Since there are only one dropdown of each type there are no need to create a class to initialize multiple elements with the same CSS class name. --- app/assets/javascripts/allowed_to_merge_dropdowns.js.es6 | 13 ------------- app/assets/javascripts/allowed_to_push_dropdowns.js.es6 | 13 ------------- app/assets/javascripts/protected_branch_create.js.es6 | 10 ++++++---- 3 files changed, 6 insertions(+), 30 deletions(-) delete mode 100644 app/assets/javascripts/allowed_to_merge_dropdowns.js.es6 delete mode 100644 app/assets/javascripts/allowed_to_push_dropdowns.js.es6 (limited to 'app') diff --git a/app/assets/javascripts/allowed_to_merge_dropdowns.js.es6 b/app/assets/javascripts/allowed_to_merge_dropdowns.js.es6 deleted file mode 100644 index 3f62ad26f0f..00000000000 --- a/app/assets/javascripts/allowed_to_merge_dropdowns.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -class AllowedToMergeDropdowns { - constructor (options) { - const { $dropdowns, onSelect } = options; - - $dropdowns.each((i, el) => { - new ProtectedBranchAccessDropdown({ - $dropdown: $(el), - data: gon.merge_access_levels, - onSelect: onSelect - }); - }); - } -} diff --git a/app/assets/javascripts/allowed_to_push_dropdowns.js.es6 b/app/assets/javascripts/allowed_to_push_dropdowns.js.es6 deleted file mode 100644 index 055be38ccdb..00000000000 --- a/app/assets/javascripts/allowed_to_push_dropdowns.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -class AllowedToPushDropdowns { - constructor (options) { - const { $dropdowns, onSelect } = options; - - $dropdowns.each((i, el) => { - new ProtectedBranchAccessDropdown({ - $dropdown: $(el), - data: gon.push_access_levels, - onSelect: onSelect - }); - }); - } -} \ No newline at end of file diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index efe91478b66..0b57964a0eb 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -6,14 +6,16 @@ class ProtectedBranchCreate { buildDropdowns() { // Allowed to Merge dropdowns - new AllowedToMergeDropdowns({ - $dropdowns: this.$wrap.find('.js-allowed-to-merge'), + new ProtectedBranchAccessDropdown({ + $dropdown: this.$wrap.find('.js-allowed-to-merge'), + data: gon.merge_access_levels, onSelect: this.onSelect.bind(this) }); // Allowed to Push dropdowns - new AllowedToPushDropdowns({ - $dropdowns: this.$wrap.find('.js-allowed-to-push'), + new ProtectedBranchAccessDropdown({ + $dropdown: this.$wrap.find('.js-allowed-to-push'), + data: gon.push_access_levels, onSelect: this.onSelect.bind(this) }); -- cgit v1.2.3 From e1d86ec9149a328bf975f55d003b3c29806cadbb Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 3 Aug 2016 20:08:57 -0500 Subject: Select first value by default --- app/assets/javascripts/protected_branch_create.js.es6 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index 0b57964a0eb..44e60e90998 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -6,19 +6,28 @@ class ProtectedBranchCreate { buildDropdowns() { // Allowed to Merge dropdowns + const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + new ProtectedBranchAccessDropdown({ - $dropdown: this.$wrap.find('.js-allowed-to-merge'), + $dropdown: $allowedToMergeDropdown, data: gon.merge_access_levels, onSelect: this.onSelect.bind(this) }); + // Select default + $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(document.createEvent('Event'), 0); + // Allowed to Push dropdowns new ProtectedBranchAccessDropdown({ - $dropdown: this.$wrap.find('.js-allowed-to-push'), + $dropdown: $allowedToPushDropdown, data: gon.push_access_levels, onSelect: this.onSelect.bind(this) }); + // Select default + $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(document.createEvent('Event'), 0); + new ProtectedBranchDropdowns({ $dropdowns: this.$wrap.find('.js-protected-branch-select'), onSelect: this.onSelect.bind(this) -- cgit v1.2.3 From c2912b46f8f51567fa7c18c90c493a94c1ffad30 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Wed, 3 Aug 2016 22:32:57 -0500 Subject: Add custom css class to each dropdown to fix failing spec --- app/views/projects/protected_branches/_protected_branch.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml index 5af4052de2f..e2e01ee78f8 100644 --- a/app/views/projects/protected_branches/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_protected_branch.html.haml @@ -16,12 +16,12 @@ %td = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", protected_branch.merge_access_level.access_level = dropdown_tag( (protected_branch.merge_access_level.humanize || 'Select') , - options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable', + options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container', data: { field_name: "allowed_to_merge_#{protected_branch.id}" }}) %td = hidden_field_tag "allowed_to_push_#{protected_branch.id}", protected_branch.push_access_level.access_level = dropdown_tag( (protected_branch.push_access_level.humanize || 'Select') , - options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable', + options: { toggle_class: 'js-allowed-to-push', dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container', data: { field_name: "allowed_to_push_#{protected_branch.id}" }}) - if can_admin_project %td -- cgit v1.2.3 From 3f399fe1817264246f978f542083962319ed0a82 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 Aug 2016 01:44:04 -0500 Subject: No need to pass event as a parameter We should stop passing events as a parameter since we cannot call the method programatically without faking or creating an event even when that event is not entirely required. --- app/assets/javascripts/gl_dropdown.js | 6 ++---- app/assets/javascripts/protected_branch_create.js.es6 | 4 ++-- app/assets/javascripts/protected_branch_select.js.es6 | 4 ++-- 3 files changed, 6 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index cc7e422fd89..d3394fae3f9 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -607,7 +607,7 @@ return this.dropdown.before($input); }; - GitLabDropdown.prototype.selectRowAtIndex = function(e, index) { + GitLabDropdown.prototype.selectRowAtIndex = function(index) { var $el, selector; selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a"; if (this.dropdown.find(".dropdown-toggle-page").length) { @@ -615,8 +615,6 @@ } $el = $(selector, this.dropdown); if ($el.length) { - e.preventDefault(); - e.stopImmediatePropagation(); return $el.first().trigger('click'); } }; @@ -653,7 +651,7 @@ return false; } if (currentKeyCode === 13 && currentIndex !== -1) { - return _this.selectRowAtIndex(e, $('.is-focused', _this.dropdown).closest('li').index() - 1); + return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1); } }; })(this)); diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index 44e60e90998..a3115c8d0b5 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -16,7 +16,7 @@ class ProtectedBranchCreate { }); // Select default - $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(document.createEvent('Event'), 0); + $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); // Allowed to Push dropdowns new ProtectedBranchAccessDropdown({ @@ -26,7 +26,7 @@ class ProtectedBranchCreate { }); // Select default - $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(document.createEvent('Event'), 0); + $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); new ProtectedBranchDropdowns({ $dropdowns: this.$wrap.find('.js-protected-branch-select'), diff --git a/app/assets/javascripts/protected_branch_select.js.es6 b/app/assets/javascripts/protected_branch_select.js.es6 index 6ca90453d49..e4881eea222 100644 --- a/app/assets/javascripts/protected_branch_select.js.es6 +++ b/app/assets/javascripts/protected_branch_select.js.es6 @@ -44,9 +44,9 @@ class ProtectedBranchDropdown { this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); } - onClickCreateWildcard(e) { + onClickCreateWildcard() { this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(e, 0); + this.$dropdown.data('glDropdown').selectRowAtIndex(0); } getProtectedBranches(term, callback) { -- cgit v1.2.3 From 416d21987e77479eb95ce51c09ea8d07800e30c9 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 Aug 2016 02:00:00 -0500 Subject: Move classes into its own file --- .../javascripts/protected_branch_dropdown.js.es6 | 75 ++++++++++++++++++ .../javascripts/protected_branch_dropdowns.js.es6 | 12 +++ .../javascripts/protected_branch_select.js.es6 | 88 ---------------------- 3 files changed, 87 insertions(+), 88 deletions(-) create mode 100644 app/assets/javascripts/protected_branch_dropdown.js.es6 create mode 100644 app/assets/javascripts/protected_branch_dropdowns.js.es6 delete mode 100644 app/assets/javascripts/protected_branch_select.js.es6 (limited to 'app') diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6 new file mode 100644 index 00000000000..35eaa18c86d --- /dev/null +++ b/app/assets/javascripts/protected_branch_dropdown.js.es6 @@ -0,0 +1,75 @@ +class ProtectedBranchDropdown { + constructor(options) { + this.onSelect = options.onSelect; + this.$dropdown = options.$dropdown; + this.$dropdownContainer = this.$dropdown.parent(); + this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); + this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch'); + + this.buildDropdown(); + this.bindEvents(); + + // Hide footer + this.$dropdownFooter.addClass('hidden'); + } + + buildDropdown() { + this.$dropdown.glDropdown({ + data: this.getProtectedBranches.bind(this), + filterable: true, + remote: false, + search: { + fields: ['title'] + }, + selectable: true, + toggleLabel(selected) { + return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; + }, + fieldName: 'protected_branch[name]', + text(protected_branch) { + return _.escape(protected_branch.title); + }, + id(protected_branch) { + return _.escape(protected_branch.id); + }, + onFilter: this.toggleCreateNewButton.bind(this), + clicked: (item, $el, e) => { + e.preventDefault(); + this.onSelect(); + } + }); + } + + bindEvents() { + this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); + } + + onClickCreateWildcard() { + this.$dropdown.data('glDropdown').remote.execute(); + this.$dropdown.data('glDropdown').selectRowAtIndex(0); + } + + getProtectedBranches(term, callback) { + if (this.selectedBranch) { + callback(gon.open_branches.concat(this.selectedBranch)); + } else { + callback(gon.open_branches); + } + } + + toggleCreateNewButton(branchName) { + this.selectedBranch = { + title: branchName, + id: branchName, + text: branchName + }; + + if (branchName) { + this.$dropdownContainer + .find('.create-new-protected-branch') + .html(`Create wildcard ${branchName}`); + } + + this.$dropdownFooter.toggleClass('hidden', !branchName); + } +} diff --git a/app/assets/javascripts/protected_branch_dropdowns.js.es6 b/app/assets/javascripts/protected_branch_dropdowns.js.es6 new file mode 100644 index 00000000000..51df4b02026 --- /dev/null +++ b/app/assets/javascripts/protected_branch_dropdowns.js.es6 @@ -0,0 +1,12 @@ +class ProtectedBranchDropdowns { + constructor(options) { + const { $dropdowns, onSelect } = options; + + $dropdowns.each((i, el) => { + new ProtectedBranchDropdown({ + $dropdown: $(el), + onSelect: onSelect + }); + }); + } + } diff --git a/app/assets/javascripts/protected_branch_select.js.es6 b/app/assets/javascripts/protected_branch_select.js.es6 deleted file mode 100644 index e4881eea222..00000000000 --- a/app/assets/javascripts/protected_branch_select.js.es6 +++ /dev/null @@ -1,88 +0,0 @@ -class ProtectedBranchDropdown { - constructor(options) { - this.onSelect = options.onSelect; - this.$dropdown = options.$dropdown; - this.$dropdownContainer = this.$dropdown.parent(); - this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); - this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch'); - - this.buildDropdown(); - this.bindEvents(); - - // Hide footer - this.$dropdownFooter.addClass('hidden'); - } - - buildDropdown() { - this.$dropdown.glDropdown({ - data: this.getProtectedBranches.bind(this), - filterable: true, - remote: false, - search: { - fields: ['title'] - }, - selectable: true, - toggleLabel(selected) { - return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; - }, - fieldName: 'protected_branch[name]', - text(protected_branch) { - return _.escape(protected_branch.title); - }, - id(protected_branch) { - return _.escape(protected_branch.id); - }, - onFilter: this.toggleCreateNewButton.bind(this), - clicked: (item, $el, e) => { - e.preventDefault(); - this.onSelect(); - } - }); - } - - bindEvents() { - this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this)); - } - - onClickCreateWildcard() { - this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(0); - } - - getProtectedBranches(term, callback) { - if (this.selectedBranch) { - callback(gon.open_branches.concat(this.selectedBranch)); - } else { - callback(gon.open_branches); - } - } - - toggleCreateNewButton(branchName) { - this.selectedBranch = { - title: branchName, - id: branchName, - text: branchName - }; - - if (branchName) { - this.$dropdownContainer - .find('.create-new-protected-branch') - .html(`Create wildcard ${branchName}`); - } - - this.$dropdownFooter.toggleClass('hidden', !branchName); - } -} - -class ProtectedBranchDropdowns { - constructor(options) { - const { $dropdowns, onSelect } = options; - - $dropdowns.each((i, el) => { - new ProtectedBranchDropdown({ - $dropdown: $(el), - onSelect: onSelect - }); - }); - } - } -- cgit v1.2.3 From 2df9bae7163601ba28e8367ed121dc04b99836ac Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 Aug 2016 03:35:49 -0500 Subject: Create protected branch dropdown directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since there’s only one dropdown of this type we don’t need a class to create this dropdown masively. --- app/assets/javascripts/protected_branch_create.js.es6 | 9 +++++---- app/assets/javascripts/protected_branch_dropdowns.js.es6 | 12 ------------ 2 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 app/assets/javascripts/protected_branch_dropdowns.js.es6 (limited to 'app') diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index a3115c8d0b5..659ed04d81a 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -5,7 +5,7 @@ class ProtectedBranchCreate { } buildDropdowns() { - // Allowed to Merge dropdowns + // Allowed to Merge dropdown const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); @@ -18,7 +18,7 @@ class ProtectedBranchCreate { // Select default $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); - // Allowed to Push dropdowns + // Allowed to Push dropdown new ProtectedBranchAccessDropdown({ $dropdown: $allowedToPushDropdown, data: gon.push_access_levels, @@ -28,8 +28,9 @@ class ProtectedBranchCreate { // Select default $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); - new ProtectedBranchDropdowns({ - $dropdowns: this.$wrap.find('.js-protected-branch-select'), + // Protected branch dropdown + new ProtectedBranchDropdown({ + $dropdown: this.$wrap.find('.js-protected-branch-select'), onSelect: this.onSelect.bind(this) }); } diff --git a/app/assets/javascripts/protected_branch_dropdowns.js.es6 b/app/assets/javascripts/protected_branch_dropdowns.js.es6 deleted file mode 100644 index 51df4b02026..00000000000 --- a/app/assets/javascripts/protected_branch_dropdowns.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -class ProtectedBranchDropdowns { - constructor(options) { - const { $dropdowns, onSelect } = options; - - $dropdowns.each((i, el) => { - new ProtectedBranchDropdown({ - $dropdown: $(el), - onSelect: onSelect - }); - }); - } - } -- cgit v1.2.3 From 45454c3cb2bd3c59a9f2ebd013b5ee3677d8c9a6 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Thu, 4 Aug 2016 15:20:51 -0500 Subject: Add url to Wildcard protected branches documentation --- .../projects/protected_branches/_create_protected_branch.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index 7006b340b34..e4319e3405d 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -10,7 +10,8 @@ .col-md-10 = render partial: "dropdown", locals: { f: f } .help-block - Wildcards such as + = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') + such as %code *-stable or %code production/* -- cgit v1.2.3 From 8f29c332b5f0520676c4e1466df5acc8cea5a513 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 Aug 2016 01:57:53 -0500 Subject: Wrap classes with IIFE and define it inside gl namespace --- app/assets/javascripts/dispatcher.js | 4 +- .../protected_branch_access_dropdown.js.es6 | 37 ++++---- .../javascripts/protected_branch_create.js.es6 | 93 +++++++++--------- .../javascripts/protected_branch_edit.js.es6 | 105 +++++++++++---------- .../javascripts/protected_branch_edit_list.js.es6 | 23 +++-- 5 files changed, 141 insertions(+), 121 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 6a153978cf2..20f2b1d69b5 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -173,8 +173,8 @@ new Search(); break; case 'projects:protected_branches:index': - new ProtectedBranchCreate(); - new ProtectedBranchEditList(); + new gl.ProtectedBranchCreate(); + new gl.ProtectedBranchEditList(); break; } switch (path.first()) { diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 index 045c12570bd..eb5d3cc3128 100644 --- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 @@ -1,18 +1,23 @@ -class ProtectedBranchAccessDropdown { - constructor(options) { - const { $dropdown, data, onSelect } = options; +(global => { + global.gl = global.gl || {}; - $dropdown.glDropdown({ - data: data, - selectable: true, - fieldName: $dropdown.data('field-name'), - toggleLabel(item) { - return item.text; - }, - clicked(item, $el, e) { - e.preventDefault(); - onSelect(); - } - }); + gl.ProtectedBranchAccessDropdown = class { + constructor(options) { + const { $dropdown, data, onSelect } = options; + + $dropdown.glDropdown({ + data: data, + selectable: true, + fieldName: $dropdown.data('field-name'), + toggleLabel(item) { + return item.text; + }, + clicked(item, $el, e) { + e.preventDefault(); + onSelect(); + } + }); + } } -} + +})(window); diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index 659ed04d81a..d112be2d08a 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -1,49 +1,54 @@ -class ProtectedBranchCreate { - constructor() { - this.$wrap = this.$form = $('#new_protected_branch'); - this.buildDropdowns(); - } +(global => { + global.gl = global.gl || {}; - buildDropdowns() { - // Allowed to Merge dropdown - const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); - const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - - new ProtectedBranchAccessDropdown({ - $dropdown: $allowedToMergeDropdown, - data: gon.merge_access_levels, - onSelect: this.onSelect.bind(this) - }); - - // Select default - $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); - - // Allowed to Push dropdown - new ProtectedBranchAccessDropdown({ - $dropdown: $allowedToPushDropdown, - data: gon.push_access_levels, - onSelect: this.onSelect.bind(this) - }); - - // Select default - $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); - - // Protected branch dropdown - new ProtectedBranchDropdown({ - $dropdown: this.$wrap.find('.js-protected-branch-select'), - onSelect: this.onSelect.bind(this) - }); - } + gl.ProtectedBranchCreate = class { + constructor() { + this.$wrap = this.$form = $('#new_protected_branch'); + this.buildDropdowns(); + } - // This will run after clicked callback - onSelect() { - // Enable submit button - const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); - const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); + buildDropdowns() { + // Allowed to Merge dropdown + const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + + new gl.ProtectedBranchAccessDropdown({ + $dropdown: $allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelect.bind(this) + }); + + // Select default + $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); + + // Allowed to Push dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: $allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelect.bind(this) + }); + + // Select default + $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); + + // Protected branch dropdown + new ProtectedBranchDropdown({ + $dropdown: this.$wrap.find('.js-protected-branch-select'), + onSelect: this.onSelect.bind(this) + }); + } + + // This will run after clicked callback + onSelect() { + // Enable submit button + const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); + const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); + const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); - if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ - this.$form.find('[type="submit"]').removeAttr('disabled'); + if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ + this.$form.find('[type="submit"]').removeAttr('disabled'); + } } } -} + +})(window); diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6 index 20f13427c0b..a549e4a1985 100644 --- a/app/assets/javascripts/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branch_edit.js.es6 @@ -1,56 +1,61 @@ -class ProtectedBranchEdit { - constructor(options) { - this.$wrap = options.$wrap; - this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); - this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); +(global => { + global.gl = global.gl || {}; - this.buildDropdowns(); - } + gl.ProtectedBranchEdit = class { + constructor(options) { + this.$wrap = options.$wrap; + this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); + this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - buildDropdowns() { - - // Allowed to merge dropdown - new ProtectedBranchAccessDropdown({ - $dropdown: this.$allowedToMergeDropdown, - data: gon.merge_access_levels, - onSelect: this.onSelect.bind(this) - }); - - // Allowed to push dropdown - new ProtectedBranchAccessDropdown({ - $dropdown: this.$allowedToPushDropdown, - data: gon.push_access_levels, - onSelect: this.onSelect.bind(this) - }); - } + this.buildDropdowns(); + } + + buildDropdowns() { + + // Allowed to merge dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToMergeDropdown, + data: gon.merge_access_levels, + onSelect: this.onSelect.bind(this) + }); - onSelect() { - const $allowedToMergeInput = $(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); - const $allowedToPushInput = $(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); - - $.ajax({ - type: 'POST', - url: this.$wrap.data('url'), - dataType: 'json', - data: { - _method: 'PATCH', - id: this.$wrap.data('banchId'), - protected_branch: { - merge_access_level_attributes: { - access_level: $allowedToMergeInput.val() - }, - push_access_level_attributes: { - access_level: $allowedToPushInput.val() + // Allowed to push dropdown + new gl.ProtectedBranchAccessDropdown({ + $dropdown: this.$allowedToPushDropdown, + data: gon.push_access_levels, + onSelect: this.onSelect.bind(this) + }); + } + + onSelect() { + const $allowedToMergeInput = $(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); + const $allowedToPushInput = $(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + + $.ajax({ + type: 'POST', + url: this.$wrap.data('url'), + dataType: 'json', + data: { + _method: 'PATCH', + id: this.$wrap.data('banchId'), + protected_branch: { + merge_access_level_attributes: { + access_level: $allowedToMergeInput.val() + }, + push_access_level_attributes: { + access_level: $allowedToPushInput.val() + } } + }, + success: () => { + this.$wrap.effect('highlight'); + }, + error() { + $.scrollTo(0); + new Flash('Failed to update branch!'); } - }, - success: () => { - this.$wrap.effect('highlight'); - }, - error() { - $.scrollTo(0); - new Flash('Failed to update branch!'); - } - }); + }); + } } -} + +})(window); diff --git a/app/assets/javascripts/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branch_edit_list.js.es6 index 6021ed87e2a..9ff0fd12c76 100644 --- a/app/assets/javascripts/protected_branch_edit_list.js.es6 +++ b/app/assets/javascripts/protected_branch_edit_list.js.es6 @@ -1,12 +1,17 @@ -class ProtectedBranchEditList { - constructor() { - this.$wrap = $('.protected-branches-list'); +(global => { + global.gl = global.gl || {}; - // Build edit forms - this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { - new ProtectedBranchEdit({ - $wrap: $(el) + gl.ProtectedBranchEditList = class { + constructor() { + this.$wrap = $('.protected-branches-list'); + + // Build edit forms + this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { + new gl.ProtectedBranchEdit({ + $wrap: $(el) + }); }); - }); + } } -} + +})(window); -- cgit v1.2.3 From 1983eb6c305eba1bc2c7cd72d1e5f9a9017dfa18 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 Aug 2016 02:02:34 -0500 Subject: camelCase param name --- app/assets/javascripts/protected_branch_dropdown.js.es6 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6 index 35eaa18c86d..66b1217473b 100644 --- a/app/assets/javascripts/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branch_dropdown.js.es6 @@ -26,11 +26,11 @@ class ProtectedBranchDropdown { return (selected && 'id' in selected) ? selected.title : 'Protected Branch'; }, fieldName: 'protected_branch[name]', - text(protected_branch) { - return _.escape(protected_branch.title); + text(protectedBranch) { + return _.escape(protectedBranch.title); }, - id(protected_branch) { - return _.escape(protected_branch.id); + id(protectedBranch) { + return _.escape(protectedBranch.id); }, onFilter: this.toggleCreateNewButton.bind(this), clicked: (item, $el, e) => { -- cgit v1.2.3 From bb02065a5dfbda79874ee41cb128e63b97b50a37 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 Aug 2016 02:05:58 -0500 Subject: Prevent setting HTML directly from the JS --- app/assets/javascripts/protected_branch_dropdown.js.es6 | 4 ++-- app/views/projects/protected_branches/_dropdown.html.haml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6 index 66b1217473b..6738dc8862d 100644 --- a/app/assets/javascripts/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branch_dropdown.js.es6 @@ -66,8 +66,8 @@ class ProtectedBranchDropdown { if (branchName) { this.$dropdownContainer - .find('.create-new-protected-branch') - .html(`Create wildcard ${branchName}`); + .find('.create-new-protected-branch code') + .text(branchName); } this.$dropdownFooter.toggleClass('hidden', !branchName); diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml index 1281e6664dd..a9e27df5a87 100644 --- a/app/views/projects/protected_branches/_dropdown.html.haml +++ b/app/views/projects/protected_branches/_dropdown.html.haml @@ -11,4 +11,5 @@ %ul.dropdown-footer-list %li = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do - Create new + Create wildcard + %code -- cgit v1.2.3 From 0e730a14a108dac05b25783427e8f299b0db4adc Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 Aug 2016 02:18:20 -0500 Subject: Cache onSelect callback before passing to each dropdown instance --- app/assets/javascripts/protected_branch_create.js.es6 | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index d112be2d08a..6942b6c36d6 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -8,38 +8,40 @@ } buildDropdowns() { - // Allowed to Merge dropdown const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); + // Cache callback + this.onSelectCallback = this.onSelect.bind(this); + + // Allowed to Merge dropdown new gl.ProtectedBranchAccessDropdown({ $dropdown: $allowedToMergeDropdown, data: gon.merge_access_levels, - onSelect: this.onSelect.bind(this) + onSelect: this.onSelectCallback }); - // Select default - $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); - // Allowed to Push dropdown new gl.ProtectedBranchAccessDropdown({ $dropdown: $allowedToPushDropdown, data: gon.push_access_levels, - onSelect: this.onSelect.bind(this) + onSelect: this.onSelectCallback }); // Select default $allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0); + $allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0); // Protected branch dropdown new ProtectedBranchDropdown({ $dropdown: this.$wrap.find('.js-protected-branch-select'), - onSelect: this.onSelect.bind(this) + onSelect: this.onSelectCallback }); } // This will run after clicked callback onSelect() { + // Enable submit button const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]'); const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_level_attributes][access_level]"]'); -- cgit v1.2.3 From af8a567a6386fdb0c08644ca87e98999da6e3146 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 Aug 2016 11:39:32 -0500 Subject: Fix spelling. `braches` to `branches` --- app/views/projects/protected_branches/_branches_list.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 777e96c073f..4e6d432f8fa 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -3,7 +3,7 @@ .panel-heading %b Protected branch (#{@protected_branches.size}) %p.settings-message.text-center - There are currently no protected braches, protect a branch with the form above. + There are currently no protected branches, protect a branch with the form above. - else - can_admin_project = can?(current_user, :admin_project, @project) -- cgit v1.2.3 From f9378bad06688bb4e1428d0b77debd9c6966a6ad Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 Aug 2016 15:26:53 -0500 Subject: Refine selector for form submit button --- app/assets/javascripts/protected_branch_create.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6 index 6942b6c36d6..00e20a03b04 100644 --- a/app/assets/javascripts/protected_branch_create.js.es6 +++ b/app/assets/javascripts/protected_branch_create.js.es6 @@ -48,7 +48,7 @@ const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_level_attributes][access_level]"]'); if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){ - this.$form.find('[type="submit"]').removeAttr('disabled'); + this.$form.find('input[type="submit"]').removeAttr('disabled'); } } } -- cgit v1.2.3 From fe8ff714d5b4ee7270b13ebbf8586481871f4013 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 Aug 2016 15:59:52 -0500 Subject: Fix .panel-title style Since .panel-heading is a bit different from bootstrap, using .panel-title for h3 looks different. So with this .panel-title will look good again inheriting custom properties our app sets for .panel-heading --- app/assets/stylesheets/framework/panels.scss | 5 +++++ app/views/projects/protected_branches/_branches_list.html.haml | 3 ++- .../projects/protected_branches/_create_protected_branch.html.haml | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 874416e1007..c6f30e144fd 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -23,4 +23,9 @@ margin-top: $gl-padding; } } + + .panel-title { + font-size: inherit; + line-height: inherit; + } } diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml index 4e6d432f8fa..04b19a8c5a7 100644 --- a/app/views/projects/protected_branches/_branches_list.html.haml +++ b/app/views/projects/protected_branches/_branches_list.html.haml @@ -1,7 +1,8 @@ .panel.panel-default.protected-branches-list - if @protected_branches.empty? .panel-heading - %b Protected branch (#{@protected_branches.size}) + %h3.panel-title + Protected branch (#{@protected_branches.size}) %p.settings-message.text-center There are currently no protected branches, protect a branch with the form above. - else diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index e4319e3405d..34bc465c356 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -1,7 +1,8 @@ = form_for [@project.namespace.becomes(Namespace), @project, @protected_branch] do |f| .panel.panel-default .panel-heading - %b Protect a branch + %h3.panel-title + Protect a branch .panel-body .form-horizontal .form-group -- cgit v1.2.3 From f096cc0471267e2aee289b9b60212d5cb5d259af Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 Aug 2016 17:04:10 -0500 Subject: Set for for labels and ID for dropdowns on create form --- app/assets/javascripts/protected_branch_access_dropdown.js.es6 | 1 + .../protected_branches/_create_protected_branch.html.haml | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 index eb5d3cc3128..2fbb088fa04 100644 --- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6 @@ -8,6 +8,7 @@ $dropdown.glDropdown({ data: data, selectable: true, + inputId: $dropdown.data('input-id'), fieldName: $dropdown.data('field-name'), toggleLabel(item) { return item.text; diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml index 34bc465c356..85d0c494ba8 100644 --- a/app/views/projects/protected_branches/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml @@ -6,7 +6,7 @@ .panel-body .form-horizontal .form-group - %label.col-md-2.text-right + = f.label :name, class: 'col-md-2 text-right' do Branch: .col-md-10 = render partial: "dropdown", locals: { f: f } @@ -18,19 +18,19 @@ %code production/* are supported .form-group - %label.col-md-2.text-right + %label.col-md-2.text-right{ for: 'merge_access_level_attributes' } Allowed to merge: .col-md-10 = dropdown_tag('Select', options: { toggle_class: 'js-allowed-to-merge wide', - data: { field_name: 'protected_branch[merge_access_level_attributes][access_level]' }}) + data: { field_name: 'protected_branch[merge_access_level_attributes][access_level]', input_id: 'merge_access_level_attributes' }}) .form-group - %label.col-md-2.text-right + %label.col-md-2.text-right{ for: 'push_access_level_attributes' } Allowed to push: .col-md-10 = dropdown_tag('Select', options: { toggle_class: 'js-allowed-to-push wide', - data: { field_name: 'protected_branch[push_access_level_attributes][access_level]' }}) + data: { field_name: 'protected_branch[push_access_level_attributes][access_level]', input_id: 'push_access_level_attributes' }}) .panel-footer = f.submit 'Protect', class: 'btn-create btn', disabled: true -- cgit v1.2.3 From 26c1b72d8f63d1ef24738d5df43d2dfe9cb2cf68 Mon Sep 17 00:00:00 2001 From: Alfredo Sumaran Date: Fri, 5 Aug 2016 17:09:21 -0500 Subject: Ensure we are looking for the right dropdown inside the form wrapper --- app/assets/javascripts/protected_branch_edit.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6 index a549e4a1985..8d42e268ebc 100644 --- a/app/assets/javascripts/protected_branch_edit.js.es6 +++ b/app/assets/javascripts/protected_branch_edit.js.es6 @@ -28,8 +28,8 @@ } onSelect() { - const $allowedToMergeInput = $(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); - const $allowedToPushInput = $(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); + const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`); + const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`); $.ajax({ type: 'POST', -- cgit v1.2.3 From 2aa2f52191b746df851853cf5fe9ce7249a70739 Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Sat, 6 Aug 2016 03:44:39 +0200 Subject: Enable Style/EmptyLinesAroundModuleBody cop --- app/helpers/avatars_helper.rb | 2 -- 1 file changed, 2 deletions(-) (limited to 'app') diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 6ff40c6b461..2160cf7a690 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -1,5 +1,4 @@ module AvatarsHelper - def author_avatar(commit_or_event, options = {}) user_avatar(options.merge({ user: commit_or_event.author, @@ -26,5 +25,4 @@ module AvatarsHelper mail_to(options[:user_email], avatar) end end - end -- cgit v1.2.3 From c9aa19881cf719baaea1bbb9bb00f84145a99b8b Mon Sep 17 00:00:00 2001 From: Gabriel Mazetto Date: Sat, 6 Aug 2016 04:03:01 +0200 Subject: Enable Style/SpaceAroundEqualsInParameterDefault cop --- app/controllers/registrations_controller.rb | 2 +- app/helpers/explore_helper.rb | 2 +- app/helpers/search_helper.rb | 2 +- app/models/merge_request_diff.rb | 2 +- app/models/repository.rb | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 75b78a49eab..3327f4f2b87 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -33,7 +33,7 @@ class RegistrationsController < Devise::RegistrationsController protected - def build_resource(hash=nil) + def build_resource(hash = nil) super end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 337b0aacbb5..2b1f3825adc 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -1,5 +1,5 @@ module ExploreHelper - def filter_projects_path(options={}) + def filter_projects_path(options = {}) exist_opts = { sort: params[:sort], scope: params[:scope], diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index a2bba139c17..c0195713f4a 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -107,7 +107,7 @@ module SearchHelper Sanitize.clean(str) end - def search_filter_path(options={}) + def search_filter_path(options = {}) exist_opts = { search: params[:search], project_id: params[:project_id], diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index fa0efe2d596..32cc6a3bfea 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -36,7 +36,7 @@ class MergeRequestDiff < ActiveRecord::Base real_size.presence || raw_diffs.size end - def raw_diffs(options={}) + def raw_diffs(options = {}) if options[:ignore_whitespace_change] @raw_diffs_no_whitespace ||= begin compare = Gitlab::Git::Compare.new( diff --git a/app/models/repository.rb b/app/models/repository.rb index c1170c470ea..701f867f67c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -601,7 +601,7 @@ class Repository commit(sha) end - def next_branch(name, opts={}) + def next_branch(name, opts = {}) branch_ids = self.branch_names.map do |n| next 1 if n == name result = n.match(/\A#{name}-([0-9]+)\z/) -- cgit v1.2.3 From 6907af28d05c22431b69f5ea7c0c685ac21131e9 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 8 Aug 2016 09:40:29 +0200 Subject: Use FA GitLab Icon for import project button --- app/views/projects/new.html.haml | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) (limited to 'app') diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index facdfcc9447..432887c1adb 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -46,28 +46,24 @@ %div - if github_import_enabled? = link_to new_import_github_path, class: 'btn import_github' do - = icon 'github', text: 'GitHub' + = icon('github', text: 'GitHub') %div - if bitbucket_import_enabled? - if bitbucket_import_configured? = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do - %i.fa.fa-bitbucket - Bitbucket + = icon('bitbucket', text: 'Bitbucket') - else = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do - %i.fa.fa-bitbucket - Bitbucket + = icon('bitbucket', text: 'Bitbucket') = render 'bitbucket_import_modal' %div - if gitlab_import_enabled? - if gitlab_import_configured? = link_to status_import_gitlab_path, class: 'btn import_gitlab' do - %i.fa.fa-heart - GitLab.com + = icon('gitlab', text: 'GitLab.com') - else = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do - %i.fa.fa-heart - GitLab.com + = icon('gitlab', text: 'GitLab.com') = render 'gitlab_import_modal' %div - if gitorious_import_enabled? @@ -77,23 +73,19 @@ %div - if google_code_import_enabled? = link_to new_import_google_code_path, class: 'btn import_google_code' do - %i.fa.fa-google - Google Code + = icon('google', text: 'Google Code') %div - if fogbugz_import_enabled? = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do - %i.fa.fa-bug - Fogbugz + = icon('bug', text: 'Fogbugz') %div - if git_import_enabled? = link_to "#", class: 'btn js-toggle-button import_git' do - %i.fa.fa-git - %span Repo by URL + = icon('git', text: 'Repo by URL') %div{ class: 'import_gitlab_project' } - if gitlab_project_import_enabled? = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do - %i.fa.fa-gitlab - %span GitLab export + = icon('gitlab', text: 'GitLab export') .js-toggle-content.hide = render "shared/import_form", f: f @@ -159,4 +151,4 @@ $('.import_git').click(function( event ) { $projectImportUrl = $('#project_import_url') $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled')) - }); \ No newline at end of file + }); -- cgit v1.2.3 From 1b5e2303debf00784603d6bd9d87d613de3c8091 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Thu, 28 Jul 2016 15:39:41 +0200 Subject: Use new badge template to render build status badge --- app/controllers/projects/badges_controller.rb | 3 ++- app/views/projects/badges/badge.svg.erb | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index a9f482c8787..d0f5071d2cc 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -8,8 +8,9 @@ class Projects::BadgesController < Projects::ApplicationController respond_to do |format| format.html { render_404 } + format.svg do - send_data(badge.data, type: badge.type, disposition: 'inline') + render 'badge', locals: { badge: badge.template } end end end diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb index 5a71419d3af..de461997c46 100644 --- a/app/views/projects/badges/badge.svg.erb +++ b/app/views/projects/badges/badge.svg.erb @@ -10,7 +10,7 @@ - + -- cgit v1.2.3 From 89f2be7d5867991c1fe964e8d9a94ff64c13ce61 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Mon, 8 Aug 2016 12:49:26 +0200 Subject: Improve indentation in svg badge template --- app/views/projects/badges/badge.svg.erb | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb index de461997c46..9d9a919f915 100644 --- a/app/views/projects/badges/badge.svg.erb +++ b/app/views/projects/badges/badge.svg.erb @@ -9,17 +9,28 @@ - - - + + + - <%= badge.key_text %> - <%= badge.key_text %> - <%= badge.value_text %> - <%= badge.value_text %> + + <%= badge.key_text %> + + + <%= badge.key_text %> + + + <%= badge.value_text %> + + + <%= badge.value_text %> + -- cgit v1.2.3 From f9cffe104489416d9a640557d87b94fd39ea4e7e Mon Sep 17 00:00:00 2001 From: Jeffrey Lin Date: Thu, 4 Aug 2016 13:56:01 -0400 Subject: "This file is managed by gitlab-ctl. Manual changes will be erased!" --- app/views/admin/application_settings/_form.html.haml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 23f864df147..c7fd344eea2 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -366,7 +366,9 @@ .col-sm-10 = f.select :repository_storage, repository_storage_options_for_select, {}, class: 'form-control' .help-block - You can manage the repository storage paths in your gitlab.yml configuration file + Manage repository storage paths. Learn more in the + = succeed "." do + = link_to "repository storages documentation", help_page_path("administration/repository_storages") %fieldset %legend Repository Checks -- cgit v1.2.3 From 77c8520e2ecd70520757aed0fbdf434643b60234 Mon Sep 17 00:00:00 2001 From: Yorick Peterse Date: Mon, 8 Aug 2016 16:18:13 +0200 Subject: Added concern for a faster "cache_key" method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This concern provides an optimized/simplified version of the "cache_key" method. This method is about 9 times faster than the default "cache_key" method. The produced cache keys _are_ different from the previous ones but this is worth the performance improvement. To showcase this I set up a benchmark (using benchmark-ips) that compares FasterCacheKeys#cache_key with the regular cache_key. The output of this benchmark was: Calculating ------------------------------------- cache_key 4.825k i/100ms cache_key_fast 21.723k i/100ms ------------------------------------------------- cache_key 59.422k (± 7.2%) i/s - 299.150k cache_key_fast 543.243k (± 9.2%) i/s - 2.694M Comparison: cache_key_fast: 543243.4 i/s cache_key: 59422.0 i/s - 9.14x slower To see the impact on real code I applied these changes and benchmarked Issue#referenced_merge_requests. For an issue referencing 10 merge requests these changes shaved off between 40 and 60 milliseconds. --- app/models/concerns/faster_cache_keys.rb | 16 ++++++++++++++++ app/models/issue.rb | 1 + app/models/note.rb | 1 + 3 files changed, 18 insertions(+) create mode 100644 app/models/concerns/faster_cache_keys.rb (limited to 'app') diff --git a/app/models/concerns/faster_cache_keys.rb b/app/models/concerns/faster_cache_keys.rb new file mode 100644 index 00000000000..5b14723fa2d --- /dev/null +++ b/app/models/concerns/faster_cache_keys.rb @@ -0,0 +1,16 @@ +module FasterCacheKeys + # A faster version of Rails' "cache_key" method. + # + # Rails' default "cache_key" method uses all kind of complex logic to figure + # out the cache key. In many cases this complexity and overhead may not be + # needed. + # + # This method does not do any timestamp parsing as this process is quite + # expensive and not needed when generating cache keys. This method also relies + # on the table name instead of the cache namespace name as the latter uses + # complex logic to generate the exact same value (as when using the table + # name) in 99% of the cases. + def cache_key + "#{self.class.table_name}/#{id}-#{read_attribute_before_type_cast(:updated_at)}" + end +end diff --git a/app/models/issue.rb b/app/models/issue.rb index 11f734cfc6d..d62ffb21467 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base include Sortable include Taskable include Spammable + include FasterCacheKeys DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze diff --git a/app/models/note.rb b/app/models/note.rb index b6b2ac6aa42..ddcd7f9d034 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -5,6 +5,7 @@ class Note < ActiveRecord::Base include Mentionable include Awardable include Importable + include FasterCacheKeys # Attribute containing rendered and redacted Markdown as generated by # Banzai::ObjectRenderer. -- cgit v1.2.3 From ace19ec58f39faa084e6f5372732e3c094011c8e Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 5 Aug 2016 12:44:13 -0500 Subject: Style deploy button --- app/assets/stylesheets/pages/environments.scss | 17 +++++++++++++++++ app/views/projects/deployments/_actions.haml | 6 +++--- 2 files changed, 20 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e160d676e35..e9097d41c61 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -1,5 +1,22 @@ .environments { + .commit-title { margin: 0; } + + .fa-play { + font-size: 14px; + } + + .dropdown-new { + color: $table-text-gray; + } + + .dropdown-menu { + + .fa { + margin-right: 6px; + color: $table-text-gray; + } + } } diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index f70dba224fa..f7bf3b834ef 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -2,9 +2,9 @@ .pull-right - actions = deployment.manual_actions - if actions.present? - .btn-group.inline - .btn-group - %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + .inline + .dropdown + %a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} = icon("play") %b.caret %ul.dropdown-menu.dropdown-menu-align-right -- cgit v1.2.3 From 415921a7f38a19bc907ce0b3db21e4e66e9b1ac2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 5 Aug 2016 14:08:39 -0500 Subject: Add avatar to commit message; environment style updates to match pipelines page --- app/assets/stylesheets/pages/environments.scss | 13 +++++++++++++ app/helpers/avatars_helper.rb | 2 -- app/views/projects/deployments/_commit.html.haml | 8 ++++++-- app/views/projects/environments/_environment.html.haml | 7 ++++--- app/views/projects/environments/index.html.haml | 4 ++-- 5 files changed, 25 insertions(+), 9 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index e9097d41c61..55f9d4a0011 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -19,4 +19,17 @@ color: $table-text-gray; } } + + .branch-name { + color: $gl-dark-link-color; + } +} + +.table.builds.environments { + min-width: 500px; + + .icon-container { + width: 20px; + text-align: center; + } } diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 6ff40c6b461..010246b6c23 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -8,8 +8,6 @@ module AvatarsHelper })) end - private - def user_avatar(options = {}) avatar_size = options[:size] || 16 user_name = options[:user].try(:name) || options[:user_name] diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index 0f9d9512d88..a90c8733862 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -1,12 +1,16 @@ %div.branch-commit - if deployment.ref - = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace" - · + .icon-container + = deployment.tag? ? icon('tag') : icon('code-fork') + = link_to deployment.ref, namespace_project_commits_path(@project.namespace, @project, deployment.ref), class: "monospace branch-name" + .icon-container + = custom_icon("icon_commit") = link_to deployment.short_sha, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-id monospace" %p.commit-title %span - if commit_title = deployment.commit_title + = user_avatar(user: deployment.user, size: 20) = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message" - else Cant find HEAD commit for this branch diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index e2453395602..d04967fa72f 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -2,8 +2,7 @@ %tr.environment %td - %strong - = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) + = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) %td - if last_deployment @@ -14,7 +13,9 @@ %td - if last_deployment - #{time_ago_with_tooltip(last_deployment.created_at)} + %p.finished-at + = icon("calendar") + #{time_ago_with_tooltip(last_deployment.created_at, short_format: true, skip_js: true)} %td = render 'projects/deployments/actions', deployment: last_deployment diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index a6dd34653ab..fe8ddb4716e 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -23,10 +23,10 @@ New environment - else .table-holder - %table.table.environments + %table.table.builds.environments %tbody %th Environment %th Last deployment - %th Date + %th %th = render @environments -- cgit v1.2.3 From 60a347228d90ce68eedb5bf4b75a4cad94a1dfb2 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Fri, 5 Aug 2016 14:54:17 -0500 Subject: Format environment history page --- app/views/projects/environments/show.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index a07436ad7c9..8f8c1c4ce22 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -23,13 +23,13 @@ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" - else .table-holder - %table.table.environments + %table.table.builds.environments %thead %tr %th ID %th Commit %th Build - %th Date + %th %th = render @deployments -- cgit v1.2.3 From 0a15ad31ad4bafe0a87cb0c4fd345a5f82c44799 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 8 Aug 2016 11:39:24 -0500 Subject: Add deployment ID and gravatar to environments page --- app/views/projects/deployments/_commit.html.haml | 2 +- app/views/projects/environments/_environment.html.haml | 9 ++++++--- app/views/projects/environments/index.html.haml | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/views/projects/deployments/_commit.html.haml b/app/views/projects/deployments/_commit.html.haml index a90c8733862..28813babd7b 100644 --- a/app/views/projects/deployments/_commit.html.haml +++ b/app/views/projects/deployments/_commit.html.haml @@ -10,7 +10,7 @@ %p.commit-title %span - if commit_title = deployment.commit_title - = user_avatar(user: deployment.user, size: 20) + = author_avatar(deployment.commit, size: 20) = link_to_gfm commit_title, namespace_project_commit_path(@project.namespace, @project, deployment.sha), class: "commit-row-message" - else Cant find HEAD commit for this branch diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index d04967fa72f..718eca15741 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -4,6 +4,11 @@ %td = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment) + %td + - if last_deployment + = user_avatar(user: last_deployment.user, size: 20) + %span ##{last_deployment.id} + %td - if last_deployment = render 'projects/deployments/commit', deployment: last_deployment @@ -13,9 +18,7 @@ %td - if last_deployment - %p.finished-at - = icon("calendar") - #{time_ago_with_tooltip(last_deployment.created_at, short_format: true, skip_js: true)} + #{time_ago_with_tooltip(last_deployment.created_at)} %td = render 'projects/deployments/actions', deployment: last_deployment diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index fe8ddb4716e..8bb238bae4e 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -26,6 +26,7 @@ %table.table.builds.environments %tbody %th Environment + %th Deployment ID %th Last deployment %th %th -- cgit v1.2.3 From f6c0c96b80b971ffaa7aa49553e13f6849f8ec4a Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 8 Aug 2016 11:43:12 -0500 Subject: Add gravatars to build history --- app/views/projects/deployments/_deployment.html.haml | 1 + app/views/projects/environments/_environment.html.haml | 2 +- app/views/projects/environments/index.html.haml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index baf02f1e6a0..cd95841ca5a 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -8,6 +8,7 @@ %td - if deployment.deployable = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do + = user_avatar(user: deployment.user, size: 20) = "#{deployment.deployable.name} (##{deployment.deployable.id})" %td diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml index 718eca15741..36a6162a5a8 100644 --- a/app/views/projects/environments/_environment.html.haml +++ b/app/views/projects/environments/_environment.html.haml @@ -7,7 +7,7 @@ %td - if last_deployment = user_avatar(user: last_deployment.user, size: 20) - %span ##{last_deployment.id} + %strong ##{last_deployment.id} %td - if last_deployment diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 8bb238bae4e..b3eb5b0011a 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -26,8 +26,8 @@ %table.table.builds.environments %tbody %th Environment - %th Deployment ID - %th Last deployment + %th Last Deployment + %th Commit %th %th = render @environments -- cgit v1.2.3 From ed63ead22aa9fd9fe509f3bebd73223b4ff8b8c5 Mon Sep 17 00:00:00 2001 From: Carlos Ribeiro Date: Mon, 8 Aug 2016 11:03:30 -0300 Subject: Avoid to show the original password field when password is automatically seted --- app/controllers/profiles/passwords_controller.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index c780e0983f9..6217ec5ecef 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -50,6 +50,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController flash[:notice] = "Password was successfully updated. Please login with it" redirect_to new_user_session_path else + @user.reload render 'edit' end end -- cgit v1.2.3 From 6af4efea872407bf7f3957f3009984989a3a8e8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Rodr=C3=ADguez?= Date: Mon, 8 Aug 2016 14:36:39 -0400 Subject: Update version_sorter and use new interface for faster tag sorting --- app/models/repository.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'app') diff --git a/app/models/repository.rb b/app/models/repository.rb index 701f867f67c..e56bac509a4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -636,9 +636,7 @@ class Repository def tags_sorted_by(value) case value when 'name' - # Would be better to use `sort_by` but `version_sorter` only exposes - # `sort` and `rsort` - VersionSorter.rsort(tag_names).map { |tag_name| find_tag(tag_name) } + VersionSorter.rsort(tags) { |tag| tag.name } when 'updated_desc' tags_sorted_by_committed_date.reverse when 'updated_asc' -- cgit v1.2.3 From 7e47a82899bdb10d2cdc61ce237a25bfa7f8a392 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Mon, 8 Aug 2016 20:32:10 +0200 Subject: Namespace EnableDeployKeyService under Projects --- app/controllers/projects/deploy_keys_controller.rb | 2 +- app/services/enable_deploy_key_service.rb | 14 -------------- app/services/projects/enable_deploy_key_service.rb | 17 +++++++++++++++++ 3 files changed, 18 insertions(+), 15 deletions(-) delete mode 100644 app/services/enable_deploy_key_service.rb create mode 100644 app/services/projects/enable_deploy_key_service.rb (limited to 'app') diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index ade2c54552b..529e0aa2d33 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -27,7 +27,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def enable - EnableDeployKeyService.new(@project, current_user, params).execute + Projects::EnableDeployKeyService.new(@project, current_user, params).execute redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) end diff --git a/app/services/enable_deploy_key_service.rb b/app/services/enable_deploy_key_service.rb deleted file mode 100644 index baa4a9dd2d4..00000000000 --- a/app/services/enable_deploy_key_service.rb +++ /dev/null @@ -1,14 +0,0 @@ -class EnableDeployKeyService < BaseService - def execute - key = accessible_keys.find_by(id: params[:key_id] || params[:id]) - - project.deploy_keys << key if key - key - end - - private - - def accessible_keys - current_user.accessible_deploy_keys - end -end diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb new file mode 100644 index 00000000000..3cf4264ce9b --- /dev/null +++ b/app/services/projects/enable_deploy_key_service.rb @@ -0,0 +1,17 @@ +module Projects + class EnableDeployKeyService < BaseService + def execute + key = accessible_keys.find_by(id: params[:key_id] || params[:id]) + return unless key + + project.deploy_keys << key + key + end + + private + + def accessible_keys + current_user.accessible_deploy_keys + end + end +end -- cgit v1.2.3 From 7dff0946a7ea8c47cf531067ac752cf900927c44 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Tue, 9 Aug 2016 12:35:36 +0200 Subject: Remove duplicate method reintroduced by merge --- app/controllers/projects/git_http_controller.rb | 4 ---- 1 file changed, 4 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index a10dca6afaf..b4373ef89ef 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -59,10 +59,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController render json: Gitlab::Workhorse.git_http_ok(repository, user) end - def render_not_found - render plain: 'Not Found', status: :not_found - end - def render_http_not_allowed render plain: access_check.message, status: :forbidden end -- cgit v1.2.3 From 8abc757539454e13835073318f896796b1a85faf Mon Sep 17 00:00:00 2001 From: Adam Buckland Date: Wed, 27 Jul 2016 13:47:23 +0100 Subject: Update tree view to sort folders with submodules Currently trees are sorted in the fashion: - folders - files - submodules with each section sorted alphabetically This changes to this system: - folders and submodules (sorted together) - files --- app/helpers/tree_helper.rb | 18 +++--------------- app/views/projects/tree/_tree_row.html.haml | 6 ++++++ 2 files changed, 9 insertions(+), 15 deletions(-) create mode 100644 app/views/projects/tree/_tree_row.html.haml (limited to 'app') diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index dbedf417fa5..4a76c679bad 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -4,23 +4,11 @@ module TreeHelper # # contents - A Grit::Tree object for the current tree def render_tree(tree) - # Render Folders before Files/Submodules + # Sort submodules and folders together by name ahead of files folders, files, submodules = tree.trees, tree.blobs, tree.submodules - tree = "" - - # Render folders if we have any - tree << render(partial: 'projects/tree/tree_item', collection: folders, - locals: { type: 'folder' }) if folders.present? - - # Render files if we have any - tree << render(partial: 'projects/tree/blob_item', collection: files, - locals: { type: 'file' }) if files.present? - - # Render submodules if we have any - tree << render(partial: 'projects/tree/submodule_item', - collection: submodules) if submodules.present? - + items = (folders + submodules).sort_by(&:name) + files + tree << render(partial: "projects/tree/tree_row", collection: items) if items.present? tree.html_safe end diff --git a/app/views/projects/tree/_tree_row.html.haml b/app/views/projects/tree/_tree_row.html.haml new file mode 100644 index 00000000000..0a5c6f048f7 --- /dev/null +++ b/app/views/projects/tree/_tree_row.html.haml @@ -0,0 +1,6 @@ +- if tree_row.type == :tree + = render partial: 'projects/tree/tree_item', object: tree_row, as: 'tree_item', locals: { type: 'folder' } +- elsif tree_row.type == :blob + = render partial: 'projects/tree/blob_item', object: tree_row, as: 'blob_item', locals: { type: 'file' } +- elsif tree_row.type == :commit + = render partial: 'projects/tree/submodule_item', object: tree_row, as: 'submodule_item' -- cgit v1.2.3 From 84ee2857975ed2da991604d9f93a151b66032991 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 9 Aug 2016 13:24:16 -0500 Subject: Add delimiter to project stars and forks count --- app/views/shared/projects/_project.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index b8b66d08db8..2cf958e9879 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -18,11 +18,11 @@ - if forks %span = icon('code-fork') - = project.forks_count + = number_with_delimiter(project.forks_count) - if stars %span = icon('star') - = project.star_count + = number_with_delimiter(project.star_count) %span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(project)} = visibility_level_icon(project.visibility_level, fw: false) -- cgit v1.2.3 From 3b4fb0bbdc13f5b52d1a4d30128d0d2d99b97e72 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 8 Aug 2016 16:26:33 -0500 Subject: Add build data to top of pipeline page --- app/assets/stylesheets/pages/pipelines.scss | 6 ++++++ app/views/projects/commit/_pipeline.html.haml | 12 ++++++++++++ 2 files changed, 18 insertions(+) (limited to 'app') diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 21919fe4d73..5bc8c3a7e6c 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -229,3 +229,9 @@ box-shadow: none; } } + +// Pipeline visualization +.stage-column { + display: inline-block; + vertical-align: top; +} diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 540689f4a61..a52bfa0ea55 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,4 +1,16 @@ .row-content-block.build-content.middle-block + .pipeline-visualization + - pipeline.statuses.stages.each do |stage| + - statuses = pipeline.statuses.where(stage: stage) + .stage-column + %strong + %a{name: stage} + - if stage + = stage.titleize + - statuses.each do |status| + %div= status.name + + .pull-right - if can?(current_user, :update_pipeline, pipeline.project) - if pipeline.builds.latest.failed.any?(&:retryable?) -- cgit v1.2.3 From dcb7d4148efe86ba07e9a00cb43818846d61f9d0 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Mon, 8 Aug 2016 17:42:28 -0500 Subject: Connect top level tests to each other --- app/assets/stylesheets/pages/pipelines.scss | 50 +++++++++++++++++++++++++++ app/views/projects/commit/_pipeline.html.haml | 44 +++++++++++++---------- 2 files changed, 75 insertions(+), 19 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 5bc8c3a7e6c..e79c7e59720 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -231,7 +231,57 @@ } // Pipeline visualization + +.pipeline-visualization { + position: relative; +} + .stage-column { display: inline-block; vertical-align: top; + margin-right: 40px; + + .stage-name { + margin-bottom: 15px; + font-weight: bold; + } + + .builds-container { + + } + + .build { + border: 1px solid $border-color; + position: relative; + padding: 6px 10px; + border-radius: 30px; + width: 150px; + // TODO truncate text within .build div; not on build div itself + // white-space: nowrap; + // overflow: hidden; + // text-overflow: ellipsis; + margin-bottom: 10px; + + svg { + position: relative; + top: 2px; + margin-right: 5px; + } + + &:first-child { + &::after, &::before { + content: ''; + position: absolute; + top: 50%; + right: -44px; + border-top: 1px solid $border-color; + width: 44px; + height: 1px; + } + } + &:first-child::before, &:last-child::after{ + border: 0 none; + } + + } } diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index a52bfa0ea55..72259cd0756 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,16 +1,22 @@ -.row-content-block.build-content.middle-block +.row-content-block.white + .pipeline-visualization - pipeline.statuses.stages.each do |stage| - statuses = pipeline.statuses.where(stage: stage) + - status = statuses.latest.status .stage-column - %strong + .stage-name %a{name: stage} - if stage = stage.titleize - - statuses.each do |status| - %div= status.name - + .builds-container + - statuses.each do |build| + .build + %span{class: "ci-status-link ci-status-icon-#{status}"} + = ci_icon_for_status(status) + = build.name +.row-content-block.build-content.middle-block .pull-right - if can?(current_user, :update_pipeline, pipeline.project) - if pipeline.builds.latest.failed.any?(&:retryable?) @@ -46,17 +52,17 @@ - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit - -.table-holder.pipeline-holder - %table.table.builds.pipeline - %thead - %tr - %th Status - %th Build ID - %th Name - %th - - if pipeline.project.build_coverage_enabled? - %th Coverage - %th - - pipeline.statuses.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage) +-# +-# .table-holder.pipeline-holder +-# %table.table.builds.pipeline +-# %thead +-# %tr +-# %th Status +-# %th Build ID +-# %th Name +-# %th +-# - if pipeline.project.build_coverage_enabled? +-# %th Coverage +-# %th +-# - pipeline.statuses.stages.each do |stage| +-# = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage) -- cgit v1.2.3 From 83c1ce74e01dcf95ac8f545d7584259615e8834f Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 9 Aug 2016 14:26:25 -0500 Subject: Connect all builds in visual pipeline --- app/assets/stylesheets/pages/pipelines.scss | 81 ++++++++++++++++++++++++--- app/views/projects/commit/_pipeline.html.haml | 32 ++++++----- 2 files changed, 90 insertions(+), 23 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index e79c7e59720..fa422f4f3c5 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -234,6 +234,10 @@ .pipeline-visualization { position: relative; + + ul { + padding: 0; + } } .stage-column { @@ -241,15 +245,15 @@ vertical-align: top; margin-right: 40px; + li { + list-style: none; + } + .stage-name { margin-bottom: 15px; font-weight: bold; } - .builds-container { - - } - .build { border: 1px solid $border-color; position: relative; @@ -268,20 +272,81 @@ margin-right: 5px; } + // Connect first build in each stage with right horizontal line &:first-child { - &::after, &::before { + &::after { content: ''; position: absolute; top: 50%; right: -44px; - border-top: 1px solid $border-color; + border-top: 2px solid $border-color; width: 44px; height: 1px; } } - &:first-child::before, &:last-child::after{ - border: 0 none; + + // Connect each build (except for first) with curved lines + &:not(:first-child) { + &::after, &::before { + content: ''; + top: -47px; + position: absolute; + border-bottom: 2px solid $border-color; + width: 20px; + height: 65px; + } + + // Right connecting curves + &::after { + right: -21px; + border-right: 2px solid $border-color; + border-radius: 0 0 50px 0; + -webkit-border-radius: 0 0 50px 0; + } + + // Left connecting curves + &::before { + left: -21px; + border-left: 2px solid $border-color; + border-radius: 0 0 0 50px; + -webkit-border-radius: 0 0 0 50px; + } + } + + // Connect second build to first build with smaller curved line + &:nth-child(2) { + &::after, &::before { + height: 40px; + top: -26px; + } + } + } + + &:last-child { + .build { + // Remove right connecting horizontal line from first build in last stage + &:first-child { + &::after, &::before { + border: none; + } + } + // Remove right curved connectors from all builds in last stage + &:not(:first-child) { + &::after { + border: none; + } + } } + } + &:first-child { + .build { + // Remove left curved connectors from all builds in first stage + &:not(:first-child) { + &::before { + border: none; + } + } + } } } diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 72259cd0756..06a63839107 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,20 +1,22 @@ .row-content-block.white .pipeline-visualization - - pipeline.statuses.stages.each do |stage| - - statuses = pipeline.statuses.where(stage: stage) - - status = statuses.latest.status - .stage-column - .stage-name - %a{name: stage} - - if stage - = stage.titleize - .builds-container - - statuses.each do |build| - .build - %span{class: "ci-status-link ci-status-icon-#{status}"} - = ci_icon_for_status(status) - = build.name + %ul.stage-column-list + - pipeline.statuses.stages.each do |stage| + - statuses = pipeline.statuses.where(stage: stage) + - status = statuses.latest.status + %li.stage-column + .stage-name + %a{name: stage} + - if stage + = stage.titleize + .builds-container + %ul + - statuses.each do |build| + %li.build + %span{class: "ci-status-link ci-status-icon-#{status}"} + = ci_icon_for_status(status) + = build.name .row-content-block.build-content.middle-block .pull-right @@ -52,7 +54,7 @@ - if pipeline.project.builds_enabled? && !pipeline.ci_yaml_file .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit --# + -# .table-holder.pipeline-holder -# %table.table.builds.pipeline -# %thead -- cgit v1.2.3 From a76864cc92241ef9b759fbe63282a45dea7d5e03 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 9 Aug 2016 15:00:09 -0500 Subject: Add min width and horizontally scroll pipeline graph when overflow --- app/assets/stylesheets/pages/pipelines.scss | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index fa422f4f3c5..a9ffda00278 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -234,16 +234,22 @@ .pipeline-visualization { position: relative; + width: 100%; + overflow: auto; ul { padding: 0; } } +.stage-column-list { + min-width: 1220px; +} + .stage-column { display: inline-block; vertical-align: top; - margin-right: 40px; + margin-right: 50px; li { list-style: none; @@ -278,9 +284,9 @@ content: ''; position: absolute; top: 50%; - right: -44px; + right: -54px; border-top: 2px solid $border-color; - width: 44px; + width: 54px; height: 1px; } } @@ -298,7 +304,7 @@ // Right connecting curves &::after { - right: -21px; + right: -20px; border-right: 2px solid $border-color; border-radius: 0 0 50px 0; -webkit-border-radius: 0 0 50px 0; @@ -306,7 +312,7 @@ // Left connecting curves &::before { - left: -21px; + left: -20px; border-left: 2px solid $border-color; border-radius: 0 0 0 50px; -webkit-border-radius: 0 0 0 50px; @@ -316,9 +322,14 @@ // Connect second build to first build with smaller curved line &:nth-child(2) { &::after, &::before { - height: 40px; + height: 45px; top: -26px; } + &::after { + // border-left: 2px solid $border-color; + border-top-right-radius: -50px; + -webkit-border-top-right-radius: -50px; + } } } -- cgit v1.2.3 From aeacadb3e32156a33060fea81243a4cef8fc1c3e Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 9 Aug 2016 15:08:04 -0500 Subject: Truncate build and stage names --- app/assets/stylesheets/pages/pipelines.scss | 15 ++++++++---- app/views/projects/commit/_pipeline.html.haml | 33 ++++++++++++++------------- 2 files changed, 28 insertions(+), 20 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index a9ffda00278..49225d7d0d4 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -258,6 +258,10 @@ .stage-name { margin-bottom: 15px; font-weight: bold; + width: 150px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .build { @@ -266,12 +270,15 @@ padding: 6px 10px; border-radius: 30px; width: 150px; - // TODO truncate text within .build div; not on build div itself - // white-space: nowrap; - // overflow: hidden; - // text-overflow: ellipsis; margin-bottom: 10px; + .build-content { + width: 130px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + svg { position: relative; top: 2px; diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 06a63839107..2edf660db71 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -14,9 +14,10 @@ %ul - statuses.each do |build| %li.build - %span{class: "ci-status-link ci-status-icon-#{status}"} - = ci_icon_for_status(status) - = build.name + .build-content + %span{class: "ci-status-link ci-status-icon-#{status}"} + = ci_icon_for_status(status) + = build.name .row-content-block.build-content.middle-block .pull-right @@ -55,16 +56,16 @@ .bs-callout.bs-callout-warning \.gitlab-ci.yml not found in this commit --# .table-holder.pipeline-holder --# %table.table.builds.pipeline --# %thead --# %tr --# %th Status --# %th Build ID --# %th Name --# %th --# - if pipeline.project.build_coverage_enabled? --# %th Coverage --# %th --# - pipeline.statuses.stages.each do |stage| --# = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage) +.table-holder.pipeline-holder + %table.table.builds.pipeline + %thead + %tr + %th Status + %th Build ID + %th Name + %th + - if pipeline.project.build_coverage_enabled? + %th Coverage + %th + - pipeline.statuses.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage) -- cgit v1.2.3 From 94ca1e3edea8fa8e70e828cc4127e0cb8ec8a9d1 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 9 Aug 2016 15:39:01 -0500 Subject: Fix graph scrolling bug --- app/assets/stylesheets/pages/pipelines.scss | 13 +++++++------ app/views/projects/commit/_pipeline.html.haml | 5 ++--- 2 files changed, 9 insertions(+), 9 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 49225d7d0d4..01bd131940a 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -232,20 +232,21 @@ // Pipeline visualization -.pipeline-visualization { - position: relative; +.pipeline-graph { width: 100%; overflow: auto; + white-space: nowrap; +} + +.pipeline-visualization { + position: relative; + min-width: 1220px; ul { padding: 0; } } -.stage-column-list { - min-width: 1220px; -} - .stage-column { display: inline-block; vertical-align: top; diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 2edf660db71..0c006656d1d 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -1,5 +1,4 @@ -.row-content-block.white - +.row-content-block.build-content.middle-block.pipeline-graph .pipeline-visualization %ul.stage-column-list - pipeline.statuses.stages.each do |stage| @@ -19,7 +18,7 @@ = ci_icon_for_status(status) = build.name -.row-content-block.build-content.middle-block +.row-content-block.build-content.middle-block.pipeline-graph .pull-right - if can?(current_user, :update_pipeline, pipeline.project) - if pipeline.builds.latest.failed.any?(&:retryable?) -- cgit v1.2.3 From ef767268645e8f60b7111eb9f6b6c3aa1b615661 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Date: Tue, 9 Aug 2016 16:15:09 -0500 Subject: Fix scss lint error --- app/assets/stylesheets/pages/pipelines.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 01bd131940a..ad256e2a728 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -314,8 +314,8 @@ &::after { right: -20px; border-right: 2px solid $border-color; - border-radius: 0 0 50px 0; - -webkit-border-radius: 0 0 50px 0; + border-radius: 0 0 50px; + -webkit-border-radius: 0 0 50px; } // Left connecting curves -- cgit v1.2.3 From 1e6316172b913b622379675d3d48e6837dfc1843 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Tue, 9 Aug 2016 14:08:33 -0700 Subject: Add a method in Project to return a cached value of total count of projects This is in preparation to address the DB load caused by the counting in gitlab-com/infrastructure#303. --- app/models/project.rb | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'app') diff --git a/app/models/project.rb b/app/models/project.rb index a667857d058..d306f86f783 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -378,6 +378,12 @@ class Project < ActiveRecord::Base joins(join_body).reorder('join_note_counts.amount DESC') end + + def cached_count + Rails.cache.fetch('total_project_count', expires_in: 5.minutes) do + Project.count + end + end end def repository_storage_path -- cgit v1.2.3 From 3756bbe12c922ad23dd5ab6302cf64b7bafe84ba Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon Date: Wed, 10 Aug 2016 08:44:19 +0200 Subject: Add missing space to generic badge template --- app/views/projects/badges/badge.svg.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/projects/badges/badge.svg.erb b/app/views/projects/badges/badge.svg.erb index 9d9a919f915..a5fef4fc56f 100644 --- a/app/views/projects/badges/badge.svg.erb +++ b/app/views/projects/badges/badge.svg.erb @@ -12,7 +12,7 @@ + d="M<%= badge.key_width %> 0 h<%= badge.value_width %> v20 H<%= badge.key_width %> z"/> -- cgit v1.2.3 From ef46221607442aaa5c2eb2c16b5f22f54abb9d3c Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 10 Aug 2016 11:10:04 +0200 Subject: Remove duplicate link_to statements --- app/views/projects/new.html.haml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) (limited to 'app') diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 432887c1adb..adcc984f506 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -49,21 +49,15 @@ = icon('github', text: 'GitHub') %div - if bitbucket_import_enabled? - - if bitbucket_import_configured? - = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do - = icon('bitbucket', text: 'Bitbucket') - - else - = link_to status_import_bitbucket_path, class: 'how_to_import_link btn import_bitbucket', "data-no-turbolink" => "true" do - = icon('bitbucket', text: 'Bitbucket') + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do + = icon('bitbucket', text: 'Bitbucket') + - unless bitbucket_import_configured? = render 'bitbucket_import_modal' %div - if gitlab_import_enabled? - - if gitlab_import_configured? - = link_to status_import_gitlab_path, class: 'btn import_gitlab' do - = icon('gitlab', text: 'GitLab.com') - - else - = link_to status_import_gitlab_path, class: 'how_to_import_link btn import_gitlab' do - = icon('gitlab', text: 'GitLab.com') + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do + = icon('gitlab', text: 'GitLab.com') + - unless gitlab_import_configured? = render 'gitlab_import_modal' %div - if gitorious_import_enabled? -- cgit v1.2.3 From 472a6a1c9deacafcba8569879df44039aa59a203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Wed, 10 Aug 2016 12:16:03 +0200 Subject: Used cached value of project count to reduce DB load MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable --- app/views/admin/dashboard/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 452fc25ab07..e6687f43816 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -112,7 +112,7 @@ %h4 Projects .data = link_to admin_namespaces_projects_path do - %h1= number_with_delimiter(Project.count) + %h1= number_with_delimiter(Project.cached_count) %hr = link_to('New Project', new_project_path, class: "btn btn-new") .col-sm-4 -- cgit v1.2.3 From 5689e8a0827eb9f09b071bbd4dc74dc2cb0a3e7d Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Wed, 10 Aug 2016 11:28:42 +0200 Subject: Avoid commit lookup on diff_helper --- app/helpers/diff_helper.rb | 5 ++--- app/views/projects/diffs/_file.html.haml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index f3c9ea074b4..0725c3f4c56 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -109,11 +109,10 @@ module DiffHelper end end - def diff_file_html_data(project, diff_file) - commit = commit_for_diff(diff_file) + def diff_file_html_data(project, diff_file_path, diff_commit_id) { blob_diff_path: namespace_project_blob_diff_path(project.namespace, project, - tree_join(commit.id, diff_file.file_path)), + tree_join(diff_commit_id, diff_file_path)), view: diff_view } end diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index f0a86fd6d40..8fbd89100ca 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,4 +1,4 @@ -.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file)} +.diff-file.file-holder{id: "diff-#{index}", data: diff_file_html_data(project, diff_file.file_path, diff_commit.id)} .file-title{id: "file-path-#{hexdigest(diff_file.file_path)}"} = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "#diff-#{index}" -- cgit v1.2.3 From 0012de8c8a6642db40c13273f32c396afaa629eb Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 10 Aug 2016 16:48:21 +0200 Subject: Rename lfs_enabled helper method --- app/controllers/projects/lfs_api_controller.rb | 2 +- app/controllers/projects/lfs_storage_controller.rb | 2 +- app/helpers/lfs_helper.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index 1fa9cd8f47f..ece49dcd922 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -1,7 +1,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController include LfsHelper - before_action :lfs_enabled! + before_action :require_lfs_enabled! before_action :lfs_check_access!, except: [:deprecated] def batch diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index d0895e1324c..6bde0a527ff 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -1,7 +1,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController include LfsHelper - before_action :lfs_enabled! + before_action :require_lfs_enabled! before_action :lfs_check_access! def download diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb index ae230ee1878..eb651e3687e 100644 --- a/app/helpers/lfs_helper.rb +++ b/app/helpers/lfs_helper.rb @@ -1,5 +1,5 @@ module LfsHelper - def lfs_enabled! + def require_lfs_enabled! return if Gitlab.config.lfs.enabled render( -- cgit v1.2.3 From f817eecb22517ece0344977d00ecc7ddfff30594 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 10 Aug 2016 16:49:23 +0200 Subject: Use && and || instead of if --- app/controllers/projects/lfs_storage_controller.rb | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 6bde0a527ff..a80fa525631 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -68,22 +68,13 @@ class Projects::LfsStorageController < Projects::GitHttpClientController end def store_file(oid, size, tmp_file) + # Define tmp_file_path early because we use it in "ensure" tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file) object = LfsObject.find_or_create_by(oid: oid, size: size) - if object.file.exists? - success = true - else - success = move_tmp_file_to_storage(object, tmp_file_path) - end - - if success - success = link_to_project(object) - end - - success + file_exists = object.file.exists? || move_tmp_file_to_storage(object, tmp_file_path) + file_exists && link_to_project(object) ensure - # Ensure that the tmp file is removed FileUtils.rm_f(tmp_file_path) end -- cgit v1.2.3 From 26b98bfff8c5bb7048bcbec46e028e30c46bccc5 Mon Sep 17 00:00:00 2001 From: Jacob Vosmaer Date: Wed, 10 Aug 2016 17:40:20 +0200 Subject: Improve validation of X-Gitlab-Lfs-Tmp header --- app/controllers/projects/lfs_storage_controller.rb | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index a80fa525631..69066cb40e6 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -58,13 +58,9 @@ class Projects::LfsStorageController < Projects::GitHttpClientController def tmp_filename name = request.headers['X-Gitlab-Lfs-Tmp'] - if name.present? - name.gsub!(/^.*(\\|\/)/, '') - name = name.match(/[0-9a-f]{73}/) - name[0] if name - else - nil - end + return if name.include?('/') + return unless oid.present? && name.start_with?(oid) + name end def store_file(oid, size, tmp_file) -- cgit v1.2.3 From 4955a47cb1c52168114364e45a2fccf6bc105452 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 6 Aug 2016 07:25:51 -0700 Subject: Clean up project destruction Instead of redirecting from the project service to the service and back to the model, put all destruction code in the service. Also removes a possible source of failure where run_after_commit may not destroy the project. --- app/controllers/projects_controller.rb | 2 +- app/models/project.rb | 10 ---------- app/services/delete_user_service.rb | 2 +- app/services/destroy_group_service.rb | 2 +- app/services/projects/destroy_service.rb | 8 ++++++-- 5 files changed, 9 insertions(+), 15 deletions(-) (limited to 'app') diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a6e1aa5ccc1..207f9d6a77f 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -125,7 +125,7 @@ class ProjectsController < Projects::ApplicationController def destroy return access_denied! unless can?(current_user, :remove_project, @project) - ::Projects::DestroyService.new(@project, current_user, {}).pending_delete! + ::Projects::DestroyService.new(@project, current_user, {}).async_execute flash[:alert] = "Project '#{@project.name}' will be deleted." redirect_to dashboard_projects_path diff --git a/app/models/project.rb b/app/models/project.rb index d306f86f783..3b1a53edc75 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1161,16 +1161,6 @@ class Project < ActiveRecord::Base @wiki ||= ProjectWiki.new(self, self.owner) end - def schedule_delete!(user_id, params) - # Queue this task for after the commit, so once we mark pending_delete it will run - run_after_commit do - job_id = ProjectDestroyWorker.perform_async(id, user_id, params) - Rails.logger.info("User #{user_id} scheduled destruction of project #{path_with_namespace} with job ID #{job_id}") - end - - update_attribute(:pending_delete, true) - end - def running_or_pending_build_count(force: false) Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do builds.running_or_pending.count(:all) diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb index ce79287e35a..2f237de813c 100644 --- a/app/services/delete_user_service.rb +++ b/app/services/delete_user_service.rb @@ -18,7 +18,7 @@ class DeleteUserService user.personal_projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! + ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute end user.destroy diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index 3c42ac61be4..a4ebccb5606 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -9,7 +9,7 @@ class DestroyGroupService group.projects.each do |project| # Skip repository removal because we remove directory with namespace # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).pending_delete! + ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute end group.destroy diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 882606e38d0..8a53f65aec1 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -6,8 +6,12 @@ module Projects DELETED_FLAG = '+deleted' - def pending_delete! - project.schedule_delete!(current_user.id, params) + def async_execute + project.transaction do + project.update_attribute(:pending_delete, true) + job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params) + Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.path_with_namespace} with job ID #{job_id}") + end end def execute -- cgit v1.2.3 From 29850364eccccc3ce7305f6706cea1d5d073de2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= Date: Thu, 23 Jun 2016 17:14:31 +0200 Subject: New AccessRequests API endpoints for Group & Project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also, mutualize AccessRequests and Members endpoints for Group & Project. New API documentation for the AccessRequests endpoints. Signed-off-by: Rémy Coutable --- app/models/members/project_member.rb | 1 + app/models/project.rb | 4 ++++ app/services/members/destroy_service.rb | 5 +++-- 3 files changed, 8 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index f176feddbad..18e97c969d7 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -8,6 +8,7 @@ class ProjectMember < Member # Make sure project member points only to project as it source default_value_for :source_type, SOURCE_TYPE validates_format_of :source_type, with: /\AProject\z/ + validates :access_level, inclusion: { in: Gitlab::Access.values } default_scope { where(source_type: SOURCE_TYPE) } scope :in_project, ->(project) { where(source_id: project.id) } diff --git a/app/models/project.rb b/app/models/project.rb index 3b1a53edc75..e0b28160937 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -999,6 +999,10 @@ class Project < ActiveRecord::Base project_members.find_by(user_id: user) end + def add_user(user, access_level, current_user = nil) + team.add_user(user, access_level, current_user) + end + def default_branch @default_branch ||= repository.root_ref if repository.exists? end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 15358f80208..9e3f6af628d 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -2,8 +2,9 @@ module Members class DestroyService < BaseService attr_accessor :member, :current_user - def initialize(member, user) - @member, @current_user = member, user + def initialize(member, current_user) + @member = member + @current_user = current_user end def execute -- cgit v1.2.3 From 88877afd09c2d72a11f4f53ba5309e64da34fdd8 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Wed, 10 Aug 2016 14:15:22 -0500 Subject: Fix branches page dropdown sort initial state --- app/controllers/projects/branches_controller.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index e926043f3eb..48fe81b0d74 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -1,12 +1,13 @@ class Projects::BranchesController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper + include SortingHelper # Authorize before_action :require_non_empty_project before_action :authorize_download_code! before_action :authorize_push_code!, only: [:new, :create, :destroy] def index - @sort = params[:sort].presence || 'name' + @sort = params[:sort].presence || sort_value_name @branches = BranchesFinder.new(@repository, params).execute @branches = Kaminari.paginate_array(@branches).page(params[:page]) -- cgit v1.2.3 From e3292f1ede67891feddfd896f16cfe42d3851558 Mon Sep 17 00:00:00 2001 From: Clement Ho Date: Tue, 9 Aug 2016 15:54:58 -0500 Subject: Fix awardable button mutuality loading spinners --- app/assets/javascripts/awards_handler.js | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index ea683b31f75..2c5b83e4f1e 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -161,23 +161,11 @@ $emojiButton = votesBlock.find("[data-emoji=" + mutualVote + "]").parent(); isAlreadyVoted = $emojiButton.hasClass('active'); if (isAlreadyVoted) { - this.showEmojiLoader($emojiButton); - return this.addAward(votesBlock, awardUrl, mutualVote, false, function() { - return $emojiButton.removeClass('is-loading'); - }); + this.addAward(votesBlock, awardUrl, mutualVote, false); } } }; - AwardsHandler.prototype.showEmojiLoader = function($emojiButton) { - var $loader; - $loader = $emojiButton.find('.fa-spinner'); - if (!$loader.length) { - $emojiButton.append(''); - } - return $emojiButton.addClass('is-loading'); - }; - AwardsHandler.prototype.isActive = function($emojiButton) { return $emojiButton.hasClass('active'); }; -- cgit v1.2.3 From 5dffd21cd526fadc3daa041fe88b60388dc786ed Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Thu, 28 Jul 2016 13:40:29 +0300 Subject: Fix error message for existing labels. --- app/assets/javascripts/labels_select.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 675dd5b7cea..1bb0b67d0e8 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -70,13 +70,15 @@ name: newLabelField.val(), color: newColorField.val() }, function(label) { - var errors; $newLabelCreateButton.enable(); if (label.message != null) { - errors = _.map(label.message, function(value, key) { - return key + " " + value[0]; - }); - return $newLabelError.html(errors.join("
")).show(); + var errorText = label.message; + if (_.isObject(label.message)) { + errorText = _.map(label.message, function(value, key) { + return key + " " + value[0]; + }).join('
'); + } + return $newLabelError.html(errorText).show(); } else { return $('.dropdown-menu-back', $dropdown.parent()).trigger('click'); } -- cgit v1.2.3 From fb748daf538e43efcf8884f017391bcbfccf2ea2 Mon Sep 17 00:00:00 2001 From: Thomas Balthazar Date: Wed, 10 Aug 2016 12:25:01 +0200 Subject: Replace the tinder gem by bare HTTP requests --- app/models/project_services/campfire_service.rb | 51 +++++++++++++++++++++---- 1 file changed, 44 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb index 511b2eac792..5af93860d09 100644 --- a/app/models/project_services/campfire_service.rb +++ b/app/models/project_services/campfire_service.rb @@ -1,4 +1,6 @@ class CampfireService < Service + include HTTParty + prop_accessor :token, :subdomain, :room validates :token, presence: true, if: :activated? @@ -29,18 +31,53 @@ class CampfireService < Service def execute(data) return unless supported_events.include?(data[:object_kind]) - room = gate.find_room_by_name(self.room) - return true unless room - + self.class.base_uri base_uri message = build_message(data) - - room.speak(message) + speak(self.room, message, auth) end private - def gate - @gate ||= Tinder::Campfire.new(subdomain, token: token) + def base_uri + @base_uri ||= "https://#{subdomain}.campfirenow.com" + end + + def auth + # use a dummy password, as explained in the Campfire API doc: + # https://github.com/basecamp/campfire-api#authentication + @auth ||= { + basic_auth: { + username: token, + password: 'X' + } + } + end + + # Post a message into a room, returns the message Hash in case of success. + # Returns nil otherwise. + # https://github.com/basecamp/campfire-api/blob/master/sections/messages.md#create-message + def speak(room_name, message, auth) + room = rooms(auth).find { |r| r["name"] == room_name } + return nil unless room + + path = "/room/#{room["id"]}/speak.json" + body = { + body: { + message: { + type: 'TextMessage', + body: message + } + } + } + res = self.class.post(path, auth.merge(body)) + res.code == 201 ? res : nil + end + + # Returns a list of rooms, or []. + # https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms + def rooms(auth) + res = self.class.get("/rooms.json", auth) + res.code == 200 ? res["rooms"] : [] end def build_message(push) -- cgit v1.2.3 From bbb019094b4b5999f96407b20a3c9da469dcf985 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Thu, 11 Aug 2016 10:02:27 +0200 Subject: Fix merge request new view not changing code view rendering style --- app/controllers/projects/merge_requests_controller.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app') diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2cf6a2dd1b3..358c6ebe4ef 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -142,6 +142,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def new + apply_diff_view_cookie! + build_merge_request @noteable = @merge_request -- cgit v1.2.3 From 39203f1adfc6fee3eca50f0cab99ffc597865200 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 11 Aug 2016 15:22:35 +0200 Subject: Pre-create all builds for Pipeline when a trigger is received This change simplifies a Pipeline processing by introducing a special new status: created. This status is used for all builds that are created for a pipeline. We are then processing next stages and queueing some of the builds (created -> pending) or skipping them (created -> skipped). This makes it possible to simplify and solve a few ordering problems with how previously builds were scheduled. This also allows us to visualise a full pipeline (with created builds). This also removes an after_touch used for updating a pipeline state parameters. Right now in various places we explicitly call a reload_status! on pipeline to force it to be updated and saved. --- app/controllers/projects/builds_controller.rb | 2 +- app/controllers/projects/commit_controller.rb | 4 +- .../projects/merge_requests_controller.rb | 4 +- app/controllers/projects/pipelines_controller.rb | 2 +- app/models/ci/build.rb | 12 +-- app/models/ci/pipeline.rb | 84 ++++++------------- app/models/commit_status.rb | 31 +++++-- app/models/concerns/statuseable.rb | 31 ++++--- app/services/ci/create_builds_service.rb | 62 -------------- app/services/ci/create_pipeline_builds_service.rb | 42 ++++++++++ app/services/ci/create_pipeline_service.rb | 95 +++++++++++++++++----- app/services/ci/create_trigger_request_service.rb | 17 +--- app/services/ci/process_pipeline_service.rb | 77 ++++++++++++++++++ app/services/create_commit_builds_service.rb | 69 ---------------- app/services/git_push_service.rb | 2 +- app/services/git_tag_push_service.rb | 2 +- .../projects/ci/pipelines/_pipeline.html.haml | 2 +- app/views/projects/commit/_pipeline.html.haml | 4 +- 18 files changed, 279 insertions(+), 263 deletions(-) delete mode 100644 app/services/ci/create_builds_service.rb create mode 100644 app/services/ci/create_pipeline_builds_service.rb create mode 100644 app/services/ci/process_pipeline_service.rb delete mode 100644 app/services/create_commit_builds_service.rb (limited to 'app') diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 553b62741a5..12195c3cbb8 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -6,7 +6,7 @@ class Projects::BuildsController < Projects::ApplicationController def index @scope = params[:scope] - @all_builds = project.builds + @all_builds = project.builds.relevant @builds = @all_builds.order('created_at DESC') @builds = case @scope diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index fdfe7c65b7b..f44e9bb3fd7 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -134,8 +134,8 @@ class Projects::CommitController < Projects::ApplicationController end def define_status_vars - @statuses = CommitStatus.where(pipeline: pipelines) - @builds = Ci::Build.where(pipeline: pipelines) + @statuses = CommitStatus.where(pipeline: pipelines).relevant + @builds = Ci::Build.where(pipeline: pipelines).relevant end def assign_change_commit_vars(mr_source_branch) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2cf6a2dd1b3..139680d2df9 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -160,7 +160,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @diff_notes_disabled = true @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses if @pipeline + @statuses = @pipeline.statuses.relevant if @pipeline @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count @@ -362,7 +362,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @commits_count = @merge_request.commits.count @pipeline = @merge_request.pipeline - @statuses = @pipeline.statuses if @pipeline + @statuses = @pipeline.statuses.relevant if @pipeline if @merge_request.locked_long_ago? @merge_request.unlock_mr diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 487963fdcd7..b0c72cfe4b4 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -19,7 +19,7 @@ class Projects::PipelinesController < Projects::ApplicationController end def create - @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute + @pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false) unless @pipeline.persisted? render 'new' return diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 08f396210c9..88a340379b8 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -16,7 +16,7 @@ module Ci scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } - scope :manual_actions, ->() { where(when: :manual) } + scope :manual_actions, ->() { where(when: :manual).relevant } mount_uploader :artifacts_file, ArtifactUploader mount_uploader :artifacts_metadata, ArtifactUploader @@ -65,17 +65,11 @@ module Ci end end - state_machine :status, initial: :pending do + state_machine :status do after_transition pending: :running do |build| build.execute_hooks end - # We use around_transition to create builds for next stage as soon as possible, before the `after_*` is executed - around_transition any => [:success, :failed, :canceled] do |build, block| - block.call - build.pipeline.create_next_builds(build) if build.pipeline - end - after_transition any => [:success, :failed, :canceled] do |build| build.update_coverage build.execute_hooks @@ -461,7 +455,7 @@ module Ci def build_attributes_from_config return {} unless pipeline.config_processor - + pipeline.config_processor.build_attributes(name) end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bce6a992af6..718fe3290c1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -13,11 +13,10 @@ module Ci has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', foreign_key: :commit_id validates_presence_of :sha + validates_presence_of :ref validates_presence_of :status validate :valid_commit_sha - # Invalidate object and save if when touched - after_touch :update_state after_save :keep_around_commits # ref can't be HEAD or SHA, can only be branch/tag name @@ -90,12 +89,16 @@ module Ci def cancel_running builds.running_or_pending.each(&:cancel) + + reload_status! end def retry_failed(user) builds.latest.failed.select(&:retryable?).each do |build| Ci::Build.retry(build, user) end + + reload_status! end def latest? @@ -109,37 +112,6 @@ module Ci trigger_requests.any? end - def create_builds(user, trigger_request = nil) - ## - # We persist pipeline only if there are builds available - # - return unless config_processor - - build_builds_for_stages(config_processor.stages, user, - 'success', trigger_request) && save - end - - def create_next_builds(build) - return unless config_processor - - # don't create other builds if this one is retried - latest_builds = builds.latest - return unless latest_builds.exists?(build.id) - - # get list of stages after this build - next_stages = config_processor.stages.drop_while { |stage| stage != build.stage } - next_stages.delete(build.stage) - - # get status for all prior builds - prior_builds = latest_builds.where.not(stage: next_stages) - prior_status = prior_builds.status - - # build builds for next stage that has builds available - # and save pipeline if we have builds - build_builds_for_stages(next_stages, build.user, prior_status, - build.trigger_request) && save - end - def retried @retried ||= (statuses.order(id: :desc) - statuses.latest) end @@ -151,6 +123,14 @@ module Ci end end + def config_builds_attributes + return [] unless config_processor + + config_processor. + builds_for_ref(ref, tag?, trigger_requests.first). + sort_by { |build| build[:stage_idx] } + end + def has_warnings? builds.latest.ignored.any? end @@ -182,10 +162,6 @@ module Ci end end - def skip_ci? - git_commit_message =~ /\[(ci skip|skip ci)\]/i if git_commit_message - end - def environments builds.where.not(environment: nil).success.pluck(:environment).uniq end @@ -207,39 +183,33 @@ module Ci Note.for_commit_id(sha) end + def process! + Ci::ProcessPipelineService.new(project, user).execute(self) + reload_status! + end + def predefined_variables [ { key: 'CI_PIPELINE_ID', value: id.to_s, public: true } ] end - private - - def build_builds_for_stages(stages, user, status, trigger_request) - ## - # Note that `Array#any?` implements a short circuit evaluation, so we - # build builds only for the first stage that has builds available. - # - stages.any? do |stage| - CreateBuildsService.new(self). - execute(stage, user, status, trigger_request). - any?(&:active?) - end - end - - def update_state + def reload_status! statuses.reload - self.status = if yaml_errors.blank? - statuses.latest.status || 'skipped' - else - 'failed' - end + self.status = + if yaml_errors.blank? + statuses.latest.status || 'skipped' + else + 'failed' + end self.started_at = statuses.started_at self.finished_at = statuses.finished_at self.duration = statuses.latest.duration save end + private + def keep_around_commits return unless project diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 2d185c28809..20713314a25 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base self.table_name = 'ci_builds' belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id - belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true + belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :user delegate :commit, to: :pipeline @@ -25,28 +25,36 @@ class CommitStatus < ActiveRecord::Base scope :ordered, -> { order(:name) } scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } - state_machine :status, initial: :pending do + state_machine :status do event :queue do - transition skipped: :pending + transition [:created, :skipped] => :pending end event :run do transition pending: :running end + event :skip do + transition [:created, :pending] => :skipped + end + event :drop do - transition [:pending, :running] => :failed + transition [:created, :pending, :running] => :failed end event :success do - transition [:pending, :running] => :success + transition [:created, :pending, :running] => :success end event :cancel do - transition [:pending, :running] => :canceled + transition [:created, :pending, :running] => :canceled + end + + after_transition created: [:pending, :running] do |commit_status| + commit_status.update_attributes queued_at: Time.now end - after_transition pending: :running do |commit_status| + after_transition [:created, :pending] => :running do |commit_status| commit_status.update_attributes started_at: Time.now end @@ -54,13 +62,20 @@ class CommitStatus < ActiveRecord::Base commit_status.update_attributes finished_at: Time.now end - after_transition [:pending, :running] => :success do |commit_status| + after_transition [:created, :pending, :running] => :success do |commit_status| MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) end after_transition any => :failed do |commit_status| MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) end + + # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed + around_transition any => [:success, :failed, :canceled] do |commit_status, block| + block.call + + commit_status.pipeline.process! if commit_status.pipeline + end end delegate :sha, :short_sha, to: :pipeline diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb index 44c6b30f278..5d4b0a86899 100644 --- a/app/models/concerns/statuseable.rb +++ b/app/models/concerns/statuseable.rb @@ -1,18 +1,22 @@ module Statuseable extend ActiveSupport::Concern - AVAILABLE_STATUSES = %w(pending running success failed canceled skipped) + AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped] + STARTED_STATUSES = %w[running success failed skipped] + ACTIVE_STATUSES = %w[pending running] + COMPLETED_STATUSES = %w[success failed canceled] class_methods do def status_sql - builds = all.select('count(*)').to_sql - success = all.success.select('count(*)').to_sql - ignored = all.ignored.select('count(*)').to_sql if all.respond_to?(:ignored) + scope = all.relevant + builds = scope.select('count(*)').to_sql + success = scope.success.select('count(*)').to_sql + ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored) ignored ||= '0' - pending = all.pending.select('count(*)').to_sql - running = all.running.select('count(*)').to_sql - canceled = all.canceled.select('count(*)').to_sql - skipped = all.skipped.select('count(*)').to_sql + pending = scope.pending.select('count(*)').to_sql + running = scope.running.select('count(*)').to_sql + canceled = scope.canceled.select('count(*)').to_sql + skipped = scope.skipped.select('count(*)').to_sql deduce_status = "(CASE WHEN (#{builds})=0 THEN NULL @@ -48,7 +52,8 @@ module Statuseable included do validates :status, inclusion: { in: AVAILABLE_STATUSES } - state_machine :status, initial: :pending do + state_machine :status, initial: :created do + state :created, value: 'created' state :pending, value: 'pending' state :running, value: 'running' state :failed, value: 'failed' @@ -57,6 +62,8 @@ module Statuseable state :skipped, value: 'skipped' end + scope :created, -> { where(status: 'created') } + scope :relevant, -> { where.not(status: 'created') } scope :running, -> { where(status: 'running') } scope :pending, -> { where(status: 'pending') } scope :success, -> { where(status: 'success') } @@ -68,14 +75,14 @@ module Statuseable end def started? - !pending? && !canceled? && started_at + STARTED_STATUSES.include?(status) && started_at end def active? - running? || pending? + ACTIVE_STATUSES.include?(status) end def complete? - canceled? || success? || failed? + COMPLETED_STATUSES.include?(status) end end diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb deleted file mode 100644 index 4946f7076fd..00000000000 --- a/app/services/ci/create_builds_service.rb +++ /dev/null @@ -1,62 +0,0 @@ -module Ci - class CreateBuildsService - def initialize(pipeline) - @pipeline = pipeline - @config = pipeline.config_processor - end - - def execute(stage, user, status, trigger_request = nil) - builds_attrs = @config.builds_for_stage_and_ref(stage, @pipeline.ref, @pipeline.tag, trigger_request) - - # check when to create next build - builds_attrs = builds_attrs.select do |build_attrs| - case build_attrs[:when] - when 'on_success' - status == 'success' - when 'on_failure' - status == 'failed' - when 'always', 'manual' - %w(success failed).include?(status) - end - end - - # don't create the same build twice - builds_attrs.reject! do |build_attrs| - @pipeline.builds.find_by(ref: @pipeline.ref, - tag: @pipeline.tag, - trigger_request: trigger_request, - name: build_attrs[:name]) - end - - builds_attrs.map do |build_attrs| - build_attrs.slice!(:name, - :commands, - :tag_list, - :options, - :allow_failure, - :stage, - :stage_idx, - :environment, - :when, - :yaml_variables) - - build_attrs.merge!(pipeline: @pipeline, - ref: @pipeline.ref, - tag: @pipeline.tag, - trigger_request: trigger_request, - user: user, - project: @pipeline.project) - - # TODO: The proper implementation for this is in - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5295 - build_attrs[:status] = 'skipped' if build_attrs[:when] == 'manual' - - ## - # We do not persist new builds here. - # Those will be persisted when @pipeline is saved. - # - @pipeline.builds.new(build_attrs) - end - end - end -end diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb new file mode 100644 index 00000000000..005014fa1de --- /dev/null +++ b/app/services/ci/create_pipeline_builds_service.rb @@ -0,0 +1,42 @@ +module Ci + class CreatePipelineBuildsService < BaseService + attr_reader :pipeline + + def execute(pipeline) + @pipeline = pipeline + + new_builds.map do |build_attributes| + create_build(build_attributes) + end + end + + private + + def create_build(build_attributes) + build_attributes = build_attributes.merge( + pipeline: pipeline, + project: pipeline.project, + ref: pipeline.ref, + tag: pipeline.tag, + user: current_user, + trigger_request: trigger_request + ) + pipeline.builds.create(build_attributes) + end + + def new_builds + @new_builds ||= pipeline.config_builds_attributes. + reject { |build| existing_build_names.include?(build[:name]) } + end + + def existing_build_names + @existing_build_names ||= pipeline.builds.pluck(:name) + end + + def trigger_request + return @trigger_request if defined?(@trigger_request) + + @trigger_request ||= pipeline.trigger_requests.first + end + end +end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index be91bf0db85..7398fd8e10a 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -1,49 +1,100 @@ module Ci class CreatePipelineService < BaseService - def execute - pipeline = project.pipelines.new(params) - pipeline.user = current_user + attr_reader :pipeline - unless ref_names.include?(params[:ref]) - pipeline.errors.add(:base, 'Reference not found') - return pipeline + def execute(ignore_skip_ci: false, save_on_errors: true, trigger_request: nil) + @pipeline = Ci::Pipeline.new( + project: project, + ref: ref, + sha: sha, + before_sha: before_sha, + tag: tag?, + trigger_requests: Array(trigger_request), + user: current_user + ) + + unless project.builds_enabled? + return error('Pipeline is disabled') + end + + unless trigger_request || can?(current_user, :create_pipeline, project) + return error('Insufficient permissions to create a new pipeline') end - if commit - pipeline.sha = commit.id - else - pipeline.errors.add(:base, 'Commit not found') - return pipeline + unless branch? || tag? + return error('Reference not found') end - unless can?(current_user, :create_pipeline, project) - pipeline.errors.add(:base, 'Insufficient permissions to create a new pipeline') - return pipeline + unless commit + return error('Commit not found') end unless pipeline.config_processor - pipeline.errors.add(:base, pipeline.yaml_errors || 'Missing .gitlab-ci.yml file') - return pipeline + unless pipeline.ci_yaml_file + return error('Missing .gitlab-ci.yml file') + end + return error(pipeline.yaml_errors, save: save_on_errors) end - pipeline.save! + if !ignore_skip_ci && skip_ci? + return error('Creation of pipeline is skipped', save: save_on_errors) + end - unless pipeline.create_builds(current_user) - pipeline.errors.add(:base, 'No builds for this pipeline.') + unless pipeline.config_builds_attributes.present? + return error('No builds for this pipeline.') end pipeline.save + pipeline.process! pipeline end private - def ref_names - @ref_names ||= project.repository.ref_names + def skip_ci? + pipeline.git_commit_message =~ /\[(ci skip|skip ci)\]/i if pipeline.git_commit_message end def commit - @commit ||= project.commit(params[:ref]) + @commit ||= project.commit(origin_sha || origin_ref) + end + + def sha + commit.try(:id) + end + + def before_sha + params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA + end + + def origin_sha + params[:checkout_sha] || params[:after] + end + + def origin_ref + params[:ref] + end + + def branch? + project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref) + end + + def tag? + project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref) + end + + def ref + Gitlab::Git.ref_name(origin_ref) + end + + def valid_sha? + origin_sha && origin_sha != Gitlab::Git::BLANK_SHA + end + + def error(message, save: false) + pipeline.errors.add(:base, message) + pipeline.reload_status! if save + pipeline end end end diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb index 1e629cf119a..6af3c1ca5b1 100644 --- a/app/services/ci/create_trigger_request_service.rb +++ b/app/services/ci/create_trigger_request_service.rb @@ -1,20 +1,11 @@ module Ci class CreateTriggerRequestService def execute(project, trigger, ref, variables = nil) - commit = project.commit(ref) - return unless commit + trigger_request = trigger.trigger_requests.create(variables: variables) - # check if ref is tag - tag = project.repository.find_tag(ref).present? - - pipeline = project.pipelines.create(sha: commit.sha, ref: ref, tag: tag) - - trigger_request = trigger.trigger_requests.create!( - variables: variables, - pipeline: pipeline, - ) - - if pipeline.create_builds(nil, trigger_request) + pipeline = Ci::CreatePipelineService.new(project, nil, ref: ref). + execute(ignore_skip_ci: true, trigger_request: trigger_request) + if pipeline.persisted? trigger_request end end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb new file mode 100644 index 00000000000..86c4823d18a --- /dev/null +++ b/app/services/ci/process_pipeline_service.rb @@ -0,0 +1,77 @@ +module Ci + class ProcessPipelineService < BaseService + attr_reader :pipeline + + def execute(pipeline) + @pipeline = pipeline + + # This method will ensure that our pipeline does have all builds for all stages created + if created_builds.empty? + create_builds! + end + + new_builds = + stage_indexes_of_created_builds.map do |index| + process_stage(index) + end + + # Return a flag if a when builds got enqueued + new_builds.flatten.any? + end + + private + + def create_builds! + Ci::CreatePipelineBuildsService.new(project, current_user).execute(pipeline) + end + + def process_stage(index) + current_status = status_for_prior_stages(index) + + created_builds_in_stage(index).select do |build| + process_build(build, current_status) + end + end + + def process_build(build, current_status) + return false unless Statuseable::COMPLETED_STATUSES.include?(current_status) + + if valid_statuses_for_when(build.when).include?(current_status) + build.queue + true + else + build.skip + false + end + end + + def valid_statuses_for_when(value) + case value + when 'on_success' + %w[success] + when 'on_failure' + %w[failed] + when 'always' + %w[success failed] + else + [] + end + end + + def status_for_prior_stages(index) + pipeline.builds.where('stage_idx < ?', index).latest.status || 'success' + end + + def stage_indexes_of_created_builds + created_builds.order(:stage_idx).pluck('distinct stage_idx') + end + + def created_builds_in_stage(index) + created_builds.where(stage_idx: index) + end + + def created_builds + pipeline.builds.created + end + end +end diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb deleted file mode 100644 index 0b66b854dea..00000000000 --- a/app/services/create_commit_builds_service.rb +++ /dev/null @@ -1,69 +0,0 @@ -class CreateCommitBuildsService - def execute(project, user, params) - return unless project.builds_enabled? - - before_sha = params[:checkout_sha] || params[:before] - sha = params[:checkout_sha] || params[:after] - origin_ref = params[:ref] - - ref = Gitlab::Git.ref_name(origin_ref) - tag = Gitlab::Git.tag_ref?(origin_ref) - - # Skip branch removal - if sha == Gitlab::Git::BLANK_SHA - return false - end - - @pipeline = Ci::Pipeline.new( - project: project, - sha: sha, - ref: ref, - before_sha: before_sha, - tag: tag, - user: user) - - ## - # Skip creating pipeline if no gitlab-ci.yml is found - # - unless @pipeline.ci_yaml_file - return false - end - - ## - # Skip creating builds for commits that have [ci skip] - # but save pipeline object - # - if @pipeline.skip_ci? - return save_pipeline! - end - - ## - # Skip creating builds when CI config is invalid - # but save pipeline object - # - unless @pipeline.config_processor - return save_pipeline! - end - - ## - # Skip creating pipeline object if there are no builds for it. - # - unless @pipeline.create_builds(user) - @pipeline.errors.add(:base, 'No builds created') - return false - end - - save_pipeline! - end - - private - - ## - # Create a new pipeline and touch object to calculate status - # - def save_pipeline! - @pipeline.save! - @pipeline.touch - @pipeline - end -end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 3f6a177bf3a..6f521462cf3 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -69,7 +69,7 @@ class GitPushService < BaseService SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks) - CreateCommitBuildsService.new.execute(@project, current_user, build_push_data) + Ci::CreatePipelineService.new(project, current_user, build_push_data).execute ProjectCacheWorker.perform_async(@project.id) end diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index 969530c4fdc..d2b52f16fa8 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -11,7 +11,7 @@ class GitTagPushService < BaseService SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks) project.execute_hooks(@push_data.dup, :tag_push_hooks) project.execute_services(@push_data.dup, :tag_push_hooks) - CreateCommitBuildsService.new.execute(project, current_user, @push_data) + Ci::CreatePipelineService.new(project, current_user, @push_data).execute ProjectCacheWorker.perform_async(project.id) true diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 9a594877803..78709a92aed 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -33,7 +33,7 @@ Cant find HEAD commit for this branch - - stages_status = pipeline.statuses.latest.stages_status + - stages_status = pipeline.statuses.relevant.latest.stages_status - stages.each do |stage| %td.stage-cell - status = stages_status[stage] diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 540689f4a61..640abdb993f 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -46,5 +46,5 @@ - if pipeline.project.build_coverage_enabled? %th Coverage %th - - pipeline.statuses.stages.each do |stage| - = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.where(stage: stage) + - pipeline.statuses.relevant.stages.each do |stage| + = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage) -- cgit v1.2.3 From ecb3f1eb6c87ab40108a5d71a3287a205ab6fefb Mon Sep 17 00:00:00 2001 From: Christopher Bartz Date: Wed, 13 Apr 2016 14:17:42 +0200 Subject: Rename `markdown_preview` routes to `preview_markdown` --- app/assets/javascripts/dropzone_input.js | 2 +- app/assets/javascripts/markdown_preview.js | 150 --------------------------- app/assets/javascripts/preview_markdown.js | 150 +++++++++++++++++++++++++++ app/controllers/projects/wikis_controller.rb | 2 +- app/controllers/projects_controller.rb | 2 +- app/views/layouts/project.html.haml | 6 +- 6 files changed, 156 insertions(+), 156 deletions(-) delete mode 100644 app/assets/javascripts/markdown_preview.js create mode 100644 app/assets/javascripts/preview_markdown.js (limited to 'app') diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 288cce04f87..4a6fea929c7 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,5 +1,5 @@ -/*= require markdown_preview */ +/*= require preview_markdown */ (function() { this.DropzoneInput = (function() { diff --git a/app/assets/javascripts/markdown_preview.js b/app/assets/javascripts/markdown_preview.js deleted file mode 100644 index 18fc7bae09a..00000000000 --- a/app/assets/javascripts/markdown_preview.js +++ /dev/null @@ -1,150 +0,0 @@ -(function() { - var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; - - this.MarkdownPreview = (function() { - function MarkdownPreview() {} - - MarkdownPreview.prototype.referenceThreshold = 10; - - MarkdownPreview.prototype.ajaxCache = {}; - - MarkdownPreview.prototype.showPreview = function(form) { - var mdText, preview; - preview = form.find('.js-md-preview'); - mdText = form.find('textarea.markdown-area').val(); - if (mdText.trim().length === 0) { - preview.text('Nothing to preview.'); - return this.hideReferencedUsers(form); - } else { - preview.text('Loading...'); - return this.renderMarkdown(mdText, (function(_this) { - return function(response) { - preview.html(response.body); - preview.syntaxHighlight(); - return _this.renderReferencedUsers(response.references.users, form); - }; - })(this)); - } - }; - - MarkdownPreview.prototype.renderMarkdown = function(text, success) { - if (!window.markdown_preview_path) { - return; - } - if (text === this.ajaxCache.text) { - return success(this.ajaxCache.response); - } - return $.ajax({ - type: 'POST', - url: window.markdown_preview_path, - data: { - text: text - }, - dataType: 'json', - success: (function(_this) { - return function(response) { - _this.ajaxCache = { - text: text, - response: response - }; - return success(response); - }; - })(this) - }); - }; - - MarkdownPreview.prototype.hideReferencedUsers = function(form) { - var referencedUsers; - referencedUsers = form.find('.referenced-users'); - return referencedUsers.hide(); - }; - - MarkdownPreview.prototype.renderReferencedUsers = function(users, form) { - var referencedUsers; - referencedUsers = form.find('.referenced-users'); - if (referencedUsers.length) { - if (users.length >= this.referenceThreshold) { - referencedUsers.show(); - return referencedUsers.find('.js-referenced-users-count').text(users.length); - } else { - return referencedUsers.hide(); - } - } - }; - - return MarkdownPreview; - - })(); - - markdownPreview = new MarkdownPreview(); - - previewButtonSelector = '.js-md-preview-button'; - - writeButtonSelector = '.js-md-write-button'; - - lastTextareaPreviewed = null; - - $.fn.setupMarkdownPreview = function() { - var $form, form_textarea; - $form = $(this); - form_textarea = $form.find('textarea.markdown-area'); - form_textarea.on('input', function() { - return markdownPreview.hideReferencedUsers($form); - }); - return form_textarea.on('blur', function() { - return markdownPreview.showPreview($form); - }); - }; - - $(document).on('markdown-preview:show', function(e, $form) { - if (!$form) { - return; - } - lastTextareaPreviewed = $form.find('textarea.markdown-area'); - $form.find(writeButtonSelector).parent().removeClass('active'); - $form.find(previewButtonSelector).parent().addClass('active'); - $form.find('.md-write-holder').hide(); - $form.find('.md-preview-holder').show(); - return markdownPreview.showPreview($form); - }); - - $(document).on('markdown-preview:hide', function(e, $form) { - if (!$form) { - return; - } - lastTextareaPreviewed = null; - $form.find(writeButtonSelector).parent().addClass('active'); - $form.find(previewButtonSelector).parent().removeClass('active'); - $form.find('.md-write-holder').show(); - $form.find('textarea.markdown-area').focus(); - return $form.find('.md-preview-holder').hide(); - }); - - $(document).on('markdown-preview:toggle', function(e, keyboardEvent) { - var $target; - $target = $(keyboardEvent.target); - if ($target.is('textarea.markdown-area')) { - $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); - return keyboardEvent.preventDefault(); - } else if (lastTextareaPreviewed) { - $target = lastTextareaPreviewed; - $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); - return keyboardEvent.preventDefault(); - } - }); - - $(document).on('click', previewButtonSelector, function(e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - return $(document).triggerHandler('markdown-preview:show', [$form]); - }); - - $(document).on('click', writeButtonSelector, function(e) { - var $form; - e.preventDefault(); - $form = $(this).closest('form'); - return $(document).triggerHandler('markdown-preview:hide', [$form]); - }); - -}).call(this); diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js new file mode 100644 index 00000000000..5fd75799640 --- /dev/null +++ b/app/assets/javascripts/preview_markdown.js @@ -0,0 +1,150 @@ +(function() { + var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector; + + this.MarkdownPreview = (function() { + function MarkdownPreview() {} + + MarkdownPreview.prototype.referenceThreshold = 10; + + MarkdownPreview.prototype.ajaxCache = {}; + + MarkdownPreview.prototype.showPreview = function(form) { + var mdText, preview; + preview = form.find('.js-md-preview'); + mdText = form.find('textarea.markdown-area').val(); + if (mdText.trim().length === 0) { + preview.text('Nothing to preview.'); + return this.hideReferencedUsers(form); + } else { + preview.text('Loading...'); + return this.renderMarkdown(mdText, (function(_this) { + return function(response) { + preview.html(response.body); + preview.syntaxHighlight(); + return _this.renderReferencedUsers(response.references.users, form); + }; + })(this)); + } + }; + + MarkdownPreview.prototype.renderMarkdown = function(text, success) { + if (!window.preview_markdown_path) { + return; + } + if (text === this.ajaxCache.text) { + return success(this.ajaxCache.response); + } + return $.ajax({ + type: 'POST', + url: window.preview_markdown_path, + data: { + text: text + }, + dataType: 'json', + success: (function(_this) { + return function(response) { + _this.ajaxCache = { + text: text, + response: response + }; + return success(response); + }; + })(this) + }); + }; + + MarkdownPreview.prototype.hideReferencedUsers = function(form) { + var referencedUsers; + referencedUsers = form.find('.referenced-users'); + return referencedUsers.hide(); + }; + + MarkdownPreview.prototype.renderReferencedUsers = function(users, form) { + var referencedUsers; + referencedUsers = form.find('.referenced-users'); + if (referencedUsers.length) { + if (users.length >= this.referenceThreshold) { + referencedUsers.show(); + return referencedUsers.find('.js-referenced-users-count').text(users.length); + } else { + return referencedUsers.hide(); + } + } + }; + + return MarkdownPreview; + + })(); + + markdownPreview = new MarkdownPreview(); + + previewButtonSelector = '.js-md-preview-button'; + + writeButtonSelector = '.js-md-write-button'; + + lastTextareaPreviewed = null; + + $.fn.setupMarkdownPreview = function() { + var $form, form_textarea; + $form = $(this); + form_textarea = $form.find('textarea.markdown-area'); + form_textarea.on('input', function() { + return markdownPreview.hideReferencedUsers($form); + }); + return form_textarea.on('blur', function() { + return markdownPreview.showPreview($form); + }); + }; + + $(document).on('markdown-preview:show', function(e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = $form.find('textarea.markdown-area'); + $form.find(writeButtonSelector).parent().removeClass('active'); + $form.find(previewButtonSelector).parent().addClass('active'); + $form.find('.md-write-holder').hide(); + $form.find('.md-preview-holder').show(); + return markdownPreview.showPreview($form); + }); + + $(document).on('markdown-preview:hide', function(e, $form) { + if (!$form) { + return; + } + lastTextareaPreviewed = null; + $form.find(writeButtonSelector).parent().addClass('active'); + $form.find(previewButtonSelector).parent().removeClass('active'); + $form.find('.md-write-holder').show(); + $form.find('textarea.markdown-area').focus(); + return $form.find('.md-preview-holder').hide(); + }); + + $(document).on('markdown-preview:toggle', function(e, keyboardEvent) { + var $target; + $target = $(keyboardEvent.target); + if ($target.is('textarea.markdown-area')) { + $(document).triggerHandler('markdown-preview:show', [$target.closest('form')]); + return keyboardEvent.preventDefault(); + } else if (lastTextareaPreviewed) { + $target = lastTextareaPreviewed; + $(document).triggerHandler('markdown-preview:hide', [$target.closest('form')]); + return keyboardEvent.preventDefault(); + } + }); + + $(document).on('click', previewButtonSelector, function(e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + return $(document).triggerHandler('markdown-preview:show', [$form]); + }); + + $(document).on('click', writeButtonSelector, function(e) { + var $form; + e.preventDefault(); + $form = $(this).closest('form'); + return $(document).triggerHandler('markdown-preview:hide', [$form]); + }); + +}).call(this); diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index 607fe9c7fed..177ccf5eec9 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -91,7 +91,7 @@ class Projects::WikisController < Projects::ApplicationController ) end - def markdown_preview + def preview_markdown text = params[:text] ext = Gitlab::ReferenceExtractor.new(@project, current_user) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 207f9d6a77f..47efbd4a939 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -238,7 +238,7 @@ class ProjectsController < Projects::ApplicationController } end - def markdown_preview + def preview_markdown text = params[:text] ext = Gitlab::ReferenceExtractor.new(@project, current_user) diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index ee9c0366f2b..9fe94291db7 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -6,13 +6,13 @@ - content_for :scripts_body_top do - project = @target_project || @project - if @project_wiki && @page - - markdown_preview_path = namespace_project_wiki_markdown_preview_path(project.namespace, project, @page.slug) + - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug) - else - - markdown_preview_path = markdown_preview_namespace_project_path(project.namespace, project) + - preview_markdown_path = preview_markdown_namespace_project_path(project.namespace, project) - if current_user :javascript window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}"; - window.markdown_preview_path = "#{markdown_preview_path}"; + window.preview_markdown_path = "#{preview_markdown_path}"; - content_for :scripts_body do = render "layouts/init_auto_complete" if current_user -- cgit v1.2.3 From 30f9596c612abc19dd060fa3a8e8ae3d92001d45 Mon Sep 17 00:00:00 2001 From: James Lopez Date: Thu, 11 Aug 2016 16:59:37 +0200 Subject: Fix permissions check in controller, added relevant spec and updated docs --- app/controllers/import/gitlab_projects_controller.rb | 5 +++++ app/views/projects/new.html.haml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 3ec173abcdb..7d0eff37635 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -1,5 +1,6 @@ class Import::GitlabProjectsController < Import::BaseController before_action :verify_gitlab_project_import_enabled + before_action :authenticate_admin! def new @namespace_id = project_params[:namespace_id] @@ -47,4 +48,8 @@ class Import::GitlabProjectsController < Import::BaseController :path, :namespace_id, :file ) end + + def authenticate_admin! + render_404 unless current_user.is_admin? + end end diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index adcc984f506..ea4898f2107 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -77,7 +77,7 @@ = link_to "#", class: 'btn js-toggle-button import_git' do = icon('git', text: 'Repo by URL') %div{ class: 'import_gitlab_project' } - - if gitlab_project_import_enabled? + - if gitlab_project_import_enabled? && current_user.is_admin? = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do = icon('gitlab', text: 'GitLab export') -- cgit v1.2.3 From ffa75a497a23bf6f87de626fee08ff4538a12587 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 11 Aug 2016 17:23:07 +0200 Subject: Remove stage parameter from send payload --- app/models/ci/pipeline.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 9545a6e3ab9..e2663f50dd1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -19,6 +19,8 @@ module Ci after_save :keep_around_commits + delegate :stages, to: :statuses + # ref can't be HEAD or SHA, can only be branch/tag name scope :latest_successful_for, ->(ref = default_branch) do where(ref: ref).success.order(id: :desc).limit(1) -- cgit v1.2.3 From c2a1011f529c21b8b571edc0daaf1f4875509e48 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Thu, 11 Aug 2016 08:45:14 -0700 Subject: Remove unused SpamReport model; this was renamed to SpamLog --- app/models/spam_report.rb | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 app/models/spam_report.rb (limited to 'app') diff --git a/app/models/spam_report.rb b/app/models/spam_report.rb deleted file mode 100644 index cdc7321b08e..00000000000 --- a/app/models/spam_report.rb +++ /dev/null @@ -1,5 +0,0 @@ -class SpamReport < ActiveRecord::Base - belongs_to :user - - validates :user, presence: true -end -- cgit v1.2.3 From 32b579e8426094af4b7731d3e91597a377a7ba7e Mon Sep 17 00:00:00 2001 From: Douwe Maan Date: Thu, 11 Aug 2016 10:00:31 -0500 Subject: Show member roles to all users on members page --- app/helpers/members_helper.rb | 6 ------ app/views/shared/members/_member.html.haml | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) (limited to 'app') diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index ec106418f2d..877c77050be 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -6,12 +6,6 @@ module MembersHelper "#{action}_#{member.type.underscore}".to_sym end - def default_show_roles(member) - can?(current_user, action_member_permission(:update, member), member) || - can?(current_user, action_member_permission(:destroy, member), member) || - can?(current_user, action_member_permission(:admin, member), member.source) - end - def remove_member_message(member, user: nil) user = current_user if defined?(current_user) diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 5ae485f36ba..fc6e206d082 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -1,4 +1,4 @@ -- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member)) +- show_roles = local_assigns.fetch(:show_roles, true) - show_controls = local_assigns.fetch(:show_controls, true) - user = member.user -- cgit v1.2.3 From 6109daf480327581b6e2dcdfffe90464be6c7796 Mon Sep 17 00:00:00 2001 From: Scott Le Date: Thu, 28 Jul 2016 11:04:57 +0700 Subject: api for generating new merge request DRY code + fix rubocop Add more test cases Append to changelog DRY changes list find_url service for merge_requests use GET for getting merge request links remove files rename to get_url_service reduce loop add test case for cross project refactor tiny thing update changelog --- app/models/merge_request.rb | 1 + app/services/merge_requests/get_urls_service.rb | 52 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 app/services/merge_requests/get_urls_service.rb (limited to 'app') diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b1fb3ce5d69..f6d0d0c98f5 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -104,6 +104,7 @@ class MergeRequest < ActiveRecord::Base scope :from_project, ->(project) { where(source_project_id: project.id) } scope :merged, -> { with_state(:merged) } scope :closed_and_merged, -> { with_states(:closed, :merged) } + scope :from_source_branches, ->(branches) { where(source_branch: branches) } scope :join_project, -> { joins(:target_project) } scope :references_project, -> { references(:target_project) } diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb new file mode 100644 index 00000000000..501fd135e16 --- /dev/null +++ b/app/services/merge_requests/get_urls_service.rb @@ -0,0 +1,52 @@ +module MergeRequests + class GetUrlsService < BaseService + attr_reader :project + + def initialize(project) + @project = project + end + + def execute(changes) + branches = get_branches(changes) + merge_requests_map = opened_merge_requests_from_source_branches(branches) + branches.map do |branch| + existing_merge_request = merge_requests_map[branch] + if existing_merge_request + url_for_existing_merge_request(existing_merge_request) + else + url_for_new_merge_request(branch) + end + end + end + + private + + def opened_merge_requests_from_source_branches(branches) + merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches) + merge_requests.inject({}) do |hash, mr| + hash[mr.source_branch] = mr + hash + end + end + + def get_branches(changes) + changes_list = Gitlab::ChangesList.new(changes) + changes_list.map do |change| + next unless Gitlab::Git.branch_ref?(change[:ref]) + Gitlab::Git.branch_name(change[:ref]) + end.compact + end + + def url_for_new_merge_request(branch_name) + merge_request_params = { source_branch: branch_name } + url = Gitlab::Routing.url_helpers.new_namespace_project_merge_request_url(project.namespace, project, merge_request: merge_request_params) + { branch_name: branch_name, url: url, new_merge_request: true } + end + + def url_for_existing_merge_request(merge_request) + target_project = merge_request.target_project + url = Gitlab::Routing.url_helpers.namespace_project_merge_request_url(target_project.namespace, target_project, merge_request) + { branch_name: merge_request.source_branch, url: url, new_merge_request: false } + end + end +end -- cgit v1.2.3 From 478990bb3ee0aa6939b656763a97d637189f062d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 11 Aug 2016 18:37:36 +0200 Subject: Fix pipeline status change from pending to running --- app/models/ci/pipeline.rb | 3 ++- app/models/commit_status.rb | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index e2663f50dd1..bf8750ca0f6 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -187,6 +187,7 @@ module Ci def process! Ci::ProcessPipelineService.new(project, user).execute(self) + reload_status! end @@ -197,7 +198,7 @@ module Ci end def reload_status! - statuses.reload + reload self.status = if yaml_errors.blank? statuses.latest.status || 'skipped' diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 20713314a25..3ab44461179 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -76,6 +76,12 @@ class CommitStatus < ActiveRecord::Base commit_status.pipeline.process! if commit_status.pipeline end + + around_transition any => [:pending, :running] do |commit_status, block| + block.call + + commit_status.pipeline.reload_status! if commit_status.pipeline + end end delegate :sha, :short_sha, to: :pipeline -- cgit v1.2.3 From e2c01f397f2f00138d2b4e618306ee2aa141c97c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 11 Aug 2016 18:37:50 +0200 Subject: Fix tests for pipeline events --- app/models/ci/pipeline.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index bf8750ca0f6..f8c0e27a5c3 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -208,8 +208,10 @@ module Ci self.started_at = statuses.started_at self.finished_at = statuses.finished_at self.duration = statuses.latest.duration + + should_execute_hooks = status_changed? save - execute_hooks if status_changed? + execute_hooks if should_execute_hooks end private -- cgit v1.2.3 From d983c5bd4671d989edf3741d0db0a54dcef9c3b6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 11 Aug 2016 18:37:36 +0200 Subject: Verify the pipeline status after executing events on builds --- app/models/ci/pipeline.rb | 3 ++- app/models/commit_status.rb | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 718fe3290c1..8de799d6088 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -185,6 +185,7 @@ module Ci def process! Ci::ProcessPipelineService.new(project, user).execute(self) + reload_status! end @@ -195,7 +196,7 @@ module Ci end def reload_status! - statuses.reload + reload self.status = if yaml_errors.blank? statuses.latest.status || 'skipped' diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 20713314a25..3ab44461179 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -76,6 +76,12 @@ class CommitStatus < ActiveRecord::Base commit_status.pipeline.process! if commit_status.pipeline end + + around_transition any => [:pending, :running] do |commit_status, block| + block.call + + commit_status.pipeline.reload_status! if commit_status.pipeline + end end delegate :sha, :short_sha, to: :pipeline -- cgit v1.2.3 From 49f72e705fa225175834b5e6b2b1f78f1f608b9c Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 2 Aug 2016 14:01:22 +0200 Subject: Show deployment status on a MR view --- app/models/deployment.rb | 7 ++++ app/models/environment.rb | 6 ++++ app/models/merge_request.rb | 6 ++++ .../merge_requests/widget/_heading.html.haml | 40 ++++++++++++++-------- 4 files changed, 45 insertions(+), 14 deletions(-) (limited to 'app') diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 1a7cd60817e..67a4f3998ec 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -36,4 +36,11 @@ class Deployment < ActiveRecord::Base def manual_actions deployable.try(:other_actions) end + + def deployed_to(ref) + commit = project.commit(ref) + return false unless commit + + project.repository.merge_base(commit.id, sha) == commit.id + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index baed106e8c8..f6fdb8d1ecf 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -25,4 +25,10 @@ class Environment < ActiveRecord::Base def nullify_external_url self.external_url = nil if self.external_url.blank? end + + def deployed_to?(ref) + return unless last_deployment + + last_deployment.deployed_to(ref) + end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b1fb3ce5d69..85e4d1f6b51 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -590,6 +590,12 @@ class MergeRequest < ActiveRecord::Base !pipeline || pipeline.success? end + def environments + target_project.environments.select do |environment| + environment.deployed_to?(ref_path) + end + end + def state_human_name if merged? "Merged" diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 6ef640bb654..0581659b742 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,21 +1,22 @@ - if @pipeline .mr-widget-heading - - %w[success success_with_warnings skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } - = ci_icon_for_status(status) - %span - CI build - = ci_label_for_status(status) - for - - commit = @merge_request.diff_head_commit - = succeed "." do - = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" - %span.ci-coverage - = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + - @merge_request.environments.each do |environments| + - %w[success success_with_warnings skipped canceled failed running pending].each do |status| + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } + = ci_icon_for_status(status) + %span + CI build + = ci_label_for_status(status) + for + - commit = @merge_request.diff_head_commit + = succeed "." do + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" + %span.ci-coverage + = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - elsif @merge_request.has_ci? - - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - - # Remove in later versions when services like Jenkins will set CI status via Commit status API + // Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX + // Remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} @@ -42,3 +43,14 @@ .ci_widget.ci-error{style: "display:none"} = icon("times-circle") Could not connect to the CI server. Please check your settings and try again. + +- @merge_request.environments.each do |environment| + .mr-widget-heading + .ci_widget{ class: "ci-success" } + = ci_icon_for_status("success") + %span + Released to #{environment.name} + = succeed '.' do + = time_ago_with_tooltip(environment.created_at, placement: 'bottom') + - if environment.external_url + = link_to icon('external-link'), environment.external_url -- cgit v1.2.3 From 826862d48ef80ddd849b9e3cb05ef37ba7be41e9 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 3 Aug 2016 10:22:01 +0200 Subject: Tests for release status heading on MR#show --- app/views/projects/merge_requests/widget/_heading.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 0581659b742..9590c1dbbd1 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -15,8 +15,8 @@ = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - elsif @merge_request.has_ci? - // Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX - // Remove in later versions when services like Jenkins will set CI status via Commit status API + - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX + - # Remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} -- cgit v1.2.3 From b497b0ce3fc3c1882639f9c7d55f7991ce41f15d Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Wed, 3 Aug 2016 13:37:39 +0200 Subject: Incorporate feedback --- app/models/deployment.rb | 4 ++-- app/models/environment.rb | 6 +++--- app/models/merge_request.rb | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 67a4f3998ec..19b08f49d96 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -37,10 +37,10 @@ class Deployment < ActiveRecord::Base deployable.try(:other_actions) end - def deployed_to(ref) + def deployed_to?(ref) commit = project.commit(ref) return false unless commit - project.repository.merge_base(commit.id, sha) == commit.id + project.repository.is_ancestor?(commit.id, sha) end end diff --git a/app/models/environment.rb b/app/models/environment.rb index f6fdb8d1ecf..7247125f8a0 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -26,9 +26,9 @@ class Environment < ActiveRecord::Base self.external_url = nil if self.external_url.blank? end - def deployed_to?(ref) - return unless last_deployment + def deployed_from?(ref) + return false unless last_deployment - last_deployment.deployed_to(ref) + last_deployment.deployed_to?(ref) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 85e4d1f6b51..945b0d76505 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -592,7 +592,7 @@ class MergeRequest < ActiveRecord::Base def environments target_project.environments.select do |environment| - environment.deployed_to?(ref_path) + environment.deployed_from?(ref_path) end end -- cgit v1.2.3 From 03ea01946524a74773b24430c81804c2724b84b6 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Thu, 4 Aug 2016 11:29:41 +0200 Subject: CI build status not per environment --- .../merge_requests/widget/_heading.html.haml | 36 ++++++++++------------ 1 file changed, 17 insertions(+), 19 deletions(-) (limited to 'app') diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 9590c1dbbd1..16e923b831c 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,18 +1,17 @@ - if @pipeline .mr-widget-heading - - @merge_request.environments.each do |environments| - - %w[success success_with_warnings skipped canceled failed running pending].each do |status| - .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } - = ci_icon_for_status(status) - %span - CI build - = ci_label_for_status(status) - for - - commit = @merge_request.diff_head_commit - = succeed "." do - = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" - %span.ci-coverage - = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} + - %w[success success_with_warnings skipped canceled failed running pending].each do |status| + .ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) } + = ci_icon_for_status(status) + %span + CI build + = ci_label_for_status(status) + for + - commit = @merge_request.diff_head_commit + = succeed "." do + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" + %span.ci-coverage + = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'} - elsif @merge_request.has_ci? - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX @@ -48,9 +47,8 @@ .mr-widget-heading .ci_widget{ class: "ci-success" } = ci_icon_for_status("success") - %span - Released to #{environment.name} - = succeed '.' do - = time_ago_with_tooltip(environment.created_at, placement: 'bottom') - - if environment.external_url - = link_to icon('external-link'), environment.external_url + %span.hidden-sm + Released to #{environment.name}. + - external_url = environment.external_url + - if external_url + = link_to icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}"), external_url -- cgit v1.2.3 From 6a6a69f4afbe0107a75df018b662f02b5ec0166a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 11 Aug 2016 20:54:02 +0200 Subject: Use state machine for pipeline event processing --- app/models/ci/pipeline.rb | 61 +++++++++++++++++++++--------- app/models/commit_status.rb | 8 ++-- app/services/ci/create_pipeline_service.rb | 5 ++- 3 files changed, 50 insertions(+), 24 deletions(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 8de799d6088..7a0430f277a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -19,6 +19,37 @@ module Ci after_save :keep_around_commits + state_machine :status, initial: :created do + event :skip do + transition any => :skipped + end + + event :drop do + transition any => :failed + end + + event :update_status do + transition any => :pending, if: ->(pipeline) { pipeline.can_transition_to?('pending') } + transition any => :running, if: ->(pipeline) { pipeline.can_transition_to?('running') } + transition any => :failed, if: ->(pipeline) { pipeline.can_transition_to?('failed') } + transition any => :success, if: ->(pipeline) { pipeline.can_transition_to?('success') } + transition any => :canceled, if: ->(pipeline) { pipeline.can_transition_to?('canceled') } + transition any => :skipped, if: ->(pipeline) { pipeline.can_transition_to?('skipped') } + end + + after_transition [:created, :pending] => :running do |pipeline| + pipeline.update(started_at: Time.now) + end + + after_transition any => [:success, :failed, :canceled] do |pipeline| + pipeline.update(finished_at: Time.now) + end + + after_transition do |pipeline| + pipeline.update_duration + end + end + # ref can't be HEAD or SHA, can only be branch/tag name scope :latest_successful_for, ->(ref = default_branch) do where(ref: ref).success.order(id: :desc).limit(1) @@ -89,16 +120,12 @@ module Ci def cancel_running builds.running_or_pending.each(&:cancel) - - reload_status! end def retry_failed(user) builds.latest.failed.select(&:retryable?).each do |build| Ci::Build.retry(build, user) end - - reload_status! end def latest? @@ -185,8 +212,6 @@ module Ci def process! Ci::ProcessPipelineService.new(project, user).execute(self) - - reload_status! end def predefined_variables @@ -195,22 +220,22 @@ module Ci ] end - def reload_status! - reload - self.status = - if yaml_errors.blank? - statuses.latest.status || 'skipped' - else - 'failed' - end - self.started_at = statuses.started_at - self.finished_at = statuses.finished_at - self.duration = statuses.latest.duration - save + def can_transition_to?(expected_status) + latest_status == expected_status + end + + def update_duration + update(duration: statuses.latest.duration) end private + def latest_status + return 'failed' unless yaml_errors.blank? + + statuses.latest.status || 'skipped' + end + def keep_around_commits return unless project diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 3ab44461179..64ce5431d63 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -74,13 +74,13 @@ class CommitStatus < ActiveRecord::Base around_transition any => [:success, :failed, :canceled] do |commit_status, block| block.call - commit_status.pipeline.process! if commit_status.pipeline + commit_status.pipeline.try(:process!) end - around_transition any => [:pending, :running] do |commit_status, block| - block.call + # Try to update the pipeline status - commit_status.pipeline.reload_status! if commit_status.pipeline + after_transition do |commit_status, transition| + commit_status.pipeline.try(:update_status) unless transition.loopback? end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 7398fd8e10a..cde856b0186 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -37,7 +37,8 @@ module Ci end if !ignore_skip_ci && skip_ci? - return error('Creation of pipeline is skipped', save: save_on_errors) + pipeline.skip if save_on_errors + return pipeline end unless pipeline.config_builds_attributes.present? @@ -93,7 +94,7 @@ module Ci def error(message, save: false) pipeline.errors.add(:base, message) - pipeline.reload_status! if save + pipeline.drop if save pipeline end end -- cgit v1.2.3 From 4ccf39cde7356bf98bef5aae694257fb2c001e75 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Thu, 11 Aug 2016 22:54:25 +0200 Subject: Fix test failures, that did occur because of missing previously used `reload_status!` call --- app/models/ci/build.rb | 37 +++++++++++++++++++------------------ app/models/commit_status.rb | 18 ++++++++---------- 2 files changed, 27 insertions(+), 28 deletions(-) (limited to 'app') diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 88a340379b8..92dad9377c9 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -42,24 +42,25 @@ module Ci end def retry(build, user = nil) - new_build = Ci::Build.new(status: 'pending') - new_build.ref = build.ref - new_build.tag = build.tag - new_build.options = build.options - new_build.commands = build.commands - new_build.tag_list = build.tag_list - new_build.project = build.project - new_build.pipeline = build.pipeline - new_build.name = build.name - new_build.allow_failure = build.allow_failure - new_build.stage = build.stage - new_build.stage_idx = build.stage_idx - new_build.trigger_request = build.trigger_request - new_build.yaml_variables = build.yaml_variables - new_build.when = build.when - new_build.user = user - new_build.environment = build.environment - new_build.save + new_build = Ci::Build.create( + ref: build.ref, + tag: build.tag, + options: build.options, + commands: build.commands, + tag_list: build.tag_list, + project: build.project, + pipeline: build.pipeline, + name: build.name, + allow_failure: build.allow_failure, + stage: build.stage, + stage_idx: build.stage_idx, + trigger_request: build.trigger_request, + yaml_variables: build.yaml_variables, + when: build.when, + user: user, + environment: build.environment, + status_event: 'queue' + ) MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) new_build end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 64ce5431d63..522fa5d6911 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -62,14 +62,6 @@ class CommitStatus < ActiveRecord::Base commit_status.update_attributes finished_at: Time.now end - after_transition [:created, :pending, :running] => :success do |commit_status| - MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) - end - - after_transition any => :failed do |commit_status| - MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) - end - # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed around_transition any => [:success, :failed, :canceled] do |commit_status, block| block.call @@ -77,11 +69,17 @@ class CommitStatus < ActiveRecord::Base commit_status.pipeline.try(:process!) end - # Try to update the pipeline status - after_transition do |commit_status, transition| commit_status.pipeline.try(:update_status) unless transition.loopback? end + + after_transition [:created, :pending, :running] => :success do |commit_status| + MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status) + end + + after_transition any => :failed do |commit_status| + MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status) + end end delegate :sha, :short_sha, to: :pipeline -- cgit v1.2.3 From 957632b72531e30637c94af9c1f7d3246cc5a6cd Mon Sep 17 00:00:00 2001 From: ubudzisz Date: Thu, 7 Jul 2016 11:02:57 +0200 Subject: render only commit title update CHANGELOG add auto-completion into pipeline add auto-completion into pipeline add auto-completion into pipeline update changelog modify tests remove empty lines add auto-completion into pipeline update changelog modify tests switch text_field_tag into text_field add test to new field switch context into describe Update CHANGELOG render only commit title update CHANGELOG add auto-completion into pipeline add auto-completion into pipeline add auto-completion into pipeline update changelog modify tests remove empty lines add auto-completion into pipeline update changelog modify tests update changelog Update CHANGELOG add indetation add tests to pipeline ref change file name for tests change file name for spec tests remove empty line rename test it rename test name removing unexpected changes removing unexpected changes2 update changelog --- app/views/projects/pipelines/new.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index 5f4ec2e40c8..55202725b9e 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -9,7 +9,7 @@ .form-group = f.label :ref, 'Create for', class: 'control-label' .col-sm-10 - = f.text_field :ref, required: true, tabindex: 2, class: 'form-control' + = f.text_field :ref, required: true, tabindex: 2, class: 'form-control js-branch-name ui-autocomplete-input', autocomplete: :false, id: :ref .help-block Existing branch name, tag .form-actions = f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3 -- cgit v1.2.3 From cb8a425ba42e9be23b8712ed29b1db2dcc6bd139 Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 28 May 2016 19:54:17 -0700 Subject: Fix bug where destroying a namespace would not always destroy projects There is a race condition in DestroyGroupService now that projects are deleted asynchronously: 1. User attempts to delete group 2. DestroyGroupService iterates through all projects and schedules a Sidekiq job to delete each Project 3. DestroyGroupService destroys the Group, leaving all its projects without a namespace 4. Projects::DestroyService runs later but the can?(current_user, :remove_project) is `false` because the user no longer has permission to destroy projects with no namespace. 5. This leaves the project in pending_delete state with no namespace/group. Projects without a namespace or group also adds another problem: it's not possible to destroy the container registry tags, since container_registry_path_with_namespace is the wrong value. The fix is to destroy the group asynchronously and to run execute directly on Projects::DestroyService. Closes #17893 --- app/controllers/admin/groups_controller.rb | 4 ++-- app/controllers/groups_controller.rb | 4 ++-- app/models/namespace.rb | 2 ++ app/services/delete_user_service.rb | 7 ++++++- app/services/destroy_group_service.rb | 16 +++++++++++++--- app/workers/group_destroy_worker.rb | 17 +++++++++++++++++ 6 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 app/workers/group_destroy_worker.rb (limited to 'app') diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index f3a88a8e6c8..4ce18321649 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -48,9 +48,9 @@ class Admin::GroupsController < Admin::ApplicationController end def destroy - DestroyGroupService.new(@group, current_user).execute + DestroyGroupService.new(@group, current_user).async_execute - redirect_to admin_groups_path, notice: 'Group was successfully deleted.' + redirect_to admin_groups_path, alert: "Group '#{@group.name}' was scheduled for deletion." end private diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 6780a6d4d87..cb82d62616c 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -87,9 +87,9 @@ class GroupsController < Groups::ApplicationController end def destroy - DestroyGroupService.new(@group, current_user).execute + DestroyGroupService.new(@group, current_user).async_execute - redirect_to root_path, alert: "Group '#{@group.name}' was successfully deleted." + redirect_to root_path, alert: "Group '#{@group.name}' was scheduled for deletion." end protected diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 8b52cc824cd..7c29d27ce97 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -1,4 +1,6 @@ class Namespace < ActiveRecord::Base + acts_as_paranoid + include Sortable include Gitlab::ShellAdapter diff --git a/app/services/delete_user_service.rb b/app/services/delete_user_service.rb index 2f237de813c..eaff88d6463 100644 --- a/app/services/delete_user_service.rb +++ b/app/services/delete_user_service.rb @@ -21,6 +21,11 @@ class DeleteUserService ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute end - user.destroy + # Destroy the namespace after destroying the user since certain methods may depend on the namespace existing + namespace = user.namespace + user_data = user.destroy + namespace.really_destroy! + + user_data end end diff --git a/app/services/destroy_group_service.rb b/app/services/destroy_group_service.rb index a4ebccb5606..0081364b8aa 100644 --- a/app/services/destroy_group_service.rb +++ b/app/services/destroy_group_service.rb @@ -5,13 +5,23 @@ class DestroyGroupService @group, @current_user = group, user end + def async_execute + group.transaction do + # Soft delete via paranoia gem + group.destroy + job_id = GroupDestroyWorker.perform_async(group.id, current_user.id) + Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") + end + end + def execute group.projects.each do |project| + # Execute the destruction of the models immediately to ensure atomic cleanup. # Skip repository removal because we remove directory with namespace - # that contain all this repositories - ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute + # that contain all these repositories + ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute end - group.destroy + group.really_destroy! end end diff --git a/app/workers/group_destroy_worker.rb b/app/workers/group_destroy_worker.rb new file mode 100644 index 00000000000..5048746f09b --- /dev/null +++ b/app/workers/group_destroy_worker.rb @@ -0,0 +1,17 @@ +class GroupDestroyWorker + include Sidekiq::Worker + + sidekiq_options queue: :default + + def perform(group_id, user_id) + begin + group = Group.with_deleted.find(group_id) + rescue ActiveRecord::RecordNotFound + return + end + + user = User.find(user_id) + + DestroyGroupService.new(group, user).execute + end +end -- cgit v1.2.3 From d5264e8804bca70e613c418a9d346f5787c6fc7a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 12 Aug 2016 16:09:29 +0800 Subject: Simplify the name for data builder, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5620#note_13671791 --- app/models/ci/build.rb | 2 +- app/models/ci/pipeline.rb | 2 +- app/models/project_services/builds_email_service.rb | 3 +-- app/models/service.rb | 2 +- app/services/delete_branch_service.rb | 9 +++++++-- app/services/delete_tag_service.rb | 9 +++++++-- app/services/git_push_service.rb | 18 ++++++++++++++---- app/services/git_tag_push_service.rb | 20 ++++++++++++++++---- app/services/notes/post_process_service.rb | 2 +- app/services/test_hook_service.rb | 3 +-- 10 files changed, 50 insertions(+), 20 deletions(-) (limited to 'app') diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 05b11f3b115..e2b0b996b6f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -343,7 +343,7 @@ module Ci def execute_hooks return unless project - build_data = Gitlab::DataBuilder::BuildDataBuilder.build(self) + build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) project.running_or_pending_build_count(force: true) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index f8c0e27a5c3..2bfe8aa5ddd 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -222,7 +222,7 @@ module Ci end def pipeline_data - Gitlab::DataBuilder::PipelineDataBuilder.build(self) + Gitlab::DataBuilder::Pipeline.build(self) end def keep_around_commits diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb index bf8c68244a1..fa66e5864b8 100644 --- a/app/models/project_services/builds_email_service.rb +++ b/app/models/project_services/builds_email_service.rb @@ -51,8 +51,7 @@ class BuildsEmailService < Service end def test_data(project = nil, user = nil) - build = project.builds.last - Gitlab::DataBuilder::BuildDataBuilder.build(build) + Gitlab::DataBuilder::Build.build(project.builds.last) end def fields diff --git a/app/models/service.rb b/app/models/service.rb index 76f588f234d..09b4717a523 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -80,7 +80,7 @@ class Service < ActiveRecord::Base end def test_data(project, user) - Gitlab::DataBuilder::PushDataBuilder.build_sample(project, user) + Gitlab::DataBuilder::Push.build_sample(project, user) end def event_channel_names diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 33c0fdc3c9d..918eddaa53a 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -39,7 +39,12 @@ class DeleteBranchService < BaseService end def build_push_data(branch) - Gitlab::DataBuilder::PushDataBuilder - .build(project, current_user, branch.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", []) + Gitlab::DataBuilder::Push.build( + project, + current_user, + branch.target.sha, + Gitlab::Git::BLANK_SHA, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", + []) end end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index 41f8006b46c..d0cb151a010 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -33,7 +33,12 @@ class DeleteTagService < BaseService end def build_push_data(tag) - Gitlab::DataBuilder::PushDataBuilder - .build(project, current_user, tag.target.sha, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", []) + Gitlab::DataBuilder::Push.build( + project, + current_user, + tag.target.sha, + Gitlab::Git::BLANK_SHA, + "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", + []) end end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index b7c5cfb58b4..e3f25ff1597 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -138,13 +138,23 @@ class GitPushService < BaseService end def build_push_data - @push_data ||= Gitlab::DataBuilder::PushDataBuilder. - build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits) + @push_data ||= Gitlab::DataBuilder::Push.build( + @project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + push_commits) end def build_push_data_system_hook - @push_data_system ||= Gitlab::DataBuilder::PushDataBuilder. - build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], []) + @push_data_system ||= Gitlab::DataBuilder::Push.build( + @project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + []) end def push_to_existing_branch? diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb index a578aaaa3b1..e6002b03b93 100644 --- a/app/services/git_tag_push_service.rb +++ b/app/services/git_tag_push_service.rb @@ -34,12 +34,24 @@ class GitTagPushService < BaseService end end - Gitlab::DataBuilder::PushDataBuilder. - build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message) + Gitlab::DataBuilder::Push.build( + project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + commits, + message) end def build_system_push_data - Gitlab::DataBuilder::PushDataBuilder. - build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '') + Gitlab::DataBuilder::Push.build( + project, + current_user, + params[:oldrev], + params[:newrev], + params[:ref], + [], + '') end end diff --git a/app/services/notes/post_process_service.rb b/app/services/notes/post_process_service.rb index a9ee7949936..e4cd3fc7833 100644 --- a/app/services/notes/post_process_service.rb +++ b/app/services/notes/post_process_service.rb @@ -16,7 +16,7 @@ module Notes end def hook_data - Gitlab::DataBuilder::NoteDataBuilder.build(@note, @note.author) + Gitlab::DataBuilder::Note.build(@note, @note.author) end def execute_note_hooks diff --git a/app/services/test_hook_service.rb b/app/services/test_hook_service.rb index 60b85882092..280c81f7d2d 100644 --- a/app/services/test_hook_service.rb +++ b/app/services/test_hook_service.rb @@ -1,7 +1,6 @@ class TestHookService def execute(hook, current_user) - data = Gitlab::DataBuilder::PushDataBuilder. - build_sample(hook.project, current_user) + data = Gitlab::DataBuilder::Push.build_sample(hook.project, current_user) hook.execute(data, 'push_hooks') end end -- cgit v1.2.3 From 07fc2f852a0b4136b6d97c1d9773819c47e7e8e7 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 9 Aug 2016 15:11:14 +0200 Subject: Method names changed to #includes_commit? --- app/assets/stylesheets/pages/merge_requests.scss | 5 ++++- app/models/deployment.rb | 3 +-- app/models/environment.rb | 4 ++-- app/models/merge_request.rb | 4 +++- app/views/projects/merge_requests/widget/_heading.html.haml | 9 ++++++--- 5 files changed, 16 insertions(+), 9 deletions(-) (limited to 'app') diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 0a661e529f0..b4636269518 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -69,6 +69,10 @@ &.ci-success { color: $gl-success; + + a.environment { + color: inherit; + } } &.ci-success_with_warnings { @@ -126,7 +130,6 @@ &.has-conflicts .fa-exclamation-triangle { color: $gl-warning; } - } p:last-child { diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 19b08f49d96..1e338889714 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -37,8 +37,7 @@ class Deployment < ActiveRecord::Base deployable.try(:other_actions) end - def deployed_to?(ref) - commit = project.commit(ref) + def includes_commit?(commit) return false unless commit project.repository.is_ancestor?(commit.id, sha) diff --git a/app/models/environment.rb b/app/models/environment.rb index 7247125f8a0..75e6f869786 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -26,9 +26,9 @@ class Environment < ActiveRecord::Base self.external_url = nil if self.external_url.blank? end - def deployed_from?(ref) + def includes_commit?(commit) return false unless last_deployment - last_deployment.deployed_to?(ref) + last_deployment.includes_commit?(commit) end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 945b0d76505..491ee2792ec 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -591,8 +591,10 @@ class MergeRequest < ActiveRecord::Base end def environments + return unless diff_head_commit + target_project.environments.select do |environment| - environment.deployed_from?(ref_path) + environment.includes_commit?(diff_head_commit) end end diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 16e923b831c..494695a03a5 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -45,10 +45,13 @@ - @merge_request.environments.each do |environment| .mr-widget-heading - .ci_widget{ class: "ci-success" } + .ci_widget.ci-success = ci_icon_for_status("success") %span.hidden-sm - Released to #{environment.name}. + Deployed to + = succeed '.' do + = link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment), class: 'environment' - external_url = environment.external_url - if external_url - = link_to icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}"), external_url + = link_to external_url, target: '_blank' do + = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true) -- cgit v1.2.3 From 2c06cf98a6dc982caf81c2e4faba195ece9a3b77 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 12 Aug 2016 17:14:11 +0800 Subject: Fix tests. We cannot reload unless it's already saved: Not sure if this is the right fix... Or maybe we should actually merge: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5782 --- app/services/ci/create_pipeline_service.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 7398fd8e10a..dabf94fe4c4 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -93,7 +93,10 @@ module Ci def error(message, save: false) pipeline.errors.add(:base, message) - pipeline.reload_status! if save + if save + pipeline.save + pipeline.reload_status! + end pipeline end end -- cgit v1.2.3 From e1f05b932de5553462793fb88fdea2ca54072d40 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 12 Aug 2016 11:36:51 +0200 Subject: Use explicit events to transition between states --- app/models/ci/pipeline.rb | 44 ++++++++++++++++++++++++++++++++------------ app/models/commit_status.rb | 2 +- 2 files changed, 33 insertions(+), 13 deletions(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 7a0430f277a..6aef91804a2 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -20,6 +20,14 @@ module Ci after_save :keep_around_commits state_machine :status, initial: :created do + event :queue do + transition :created => :pending + end + + event :run do + transition [:pending, :success, :failed, :canceled, :skipped] => :running + end + event :skip do transition any => :skipped end @@ -28,13 +36,12 @@ module Ci transition any => :failed end - event :update_status do - transition any => :pending, if: ->(pipeline) { pipeline.can_transition_to?('pending') } - transition any => :running, if: ->(pipeline) { pipeline.can_transition_to?('running') } - transition any => :failed, if: ->(pipeline) { pipeline.can_transition_to?('failed') } - transition any => :success, if: ->(pipeline) { pipeline.can_transition_to?('success') } - transition any => :canceled, if: ->(pipeline) { pipeline.can_transition_to?('canceled') } - transition any => :skipped, if: ->(pipeline) { pipeline.can_transition_to?('skipped') } + event :succeed do + transition any => :success + end + + event :cancel do + transition any => :canceled end after_transition [:created, :pending] => :running do |pipeline| @@ -214,23 +221,36 @@ module Ci Ci::ProcessPipelineService.new(project, user).execute(self) end + def build_updated + case latest_builds_status + when 'pending' + queue + when 'running' + run + when 'success' + succeed + when 'failed' + drop + when 'canceled' + cancel + when 'skipped' + skip + end + end + def predefined_variables [ { key: 'CI_PIPELINE_ID', value: id.to_s, public: true } ] end - def can_transition_to?(expected_status) - latest_status == expected_status - end - def update_duration update(duration: statuses.latest.duration) end private - def latest_status + def latest_builds_status return 'failed' unless yaml_errors.blank? statuses.latest.status || 'skipped' diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 522fa5d6911..c21c8ce18db 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -70,7 +70,7 @@ class CommitStatus < ActiveRecord::Base end after_transition do |commit_status, transition| - commit_status.pipeline.try(:update_status) unless transition.loopback? + commit_status.pipeline.try(:build_updated) unless transition.loopback? end after_transition [:created, :pending, :running] => :success do |commit_status| -- cgit v1.2.3 From ad3e1edcfce1e24fb9889d5d73852680cf4facf9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 12 Aug 2016 11:53:27 +0200 Subject: Added specs for started_at and finished_at --- app/models/ci/pipeline.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 6aef91804a2..92fae78fe4e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -22,10 +22,11 @@ module Ci state_machine :status, initial: :created do event :queue do transition :created => :pending + transition any - [:created, :pending] => :running end event :run do - transition [:pending, :success, :failed, :canceled, :skipped] => :running + transition any => :running end event :skip do @@ -44,15 +45,15 @@ module Ci transition any => :canceled end - after_transition [:created, :pending] => :running do |pipeline| - pipeline.update(started_at: Time.now) + before_transition [:created, :pending] => :running do |pipeline| + pipeline.started_at = Time.now end - after_transition any => [:success, :failed, :canceled] do |pipeline| - pipeline.update(finished_at: Time.now) + before_transition any => [:success, :failed, :canceled] do |pipeline| + pipeline.finished_at = Time.now end - after_transition do |pipeline| + before_transition do |pipeline| pipeline.update_duration end end @@ -245,7 +246,7 @@ module Ci end def update_duration - update(duration: statuses.latest.duration) + self.duration = statuses.latest.duration end private -- cgit v1.2.3 From 706d872eb2ebb462b5c226890120f51cf15ba7c5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 12 Aug 2016 12:06:37 +0200 Subject: Make `execute_methods` public --- app/models/ci/pipeline.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 40615097804..ad836bbebb8 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -255,13 +255,13 @@ module Ci self.duration = statuses.latest.duration end - private - def execute_hooks project.execute_hooks(pipeline_data, :pipeline_hooks) project.execute_services(pipeline_data, :pipeline_hooks) end + private + def pipeline_data Gitlab::DataBuilder::Pipeline.build(self) end -- cgit v1.2.3 From d7b681512bb738b9b2ca0c56e761616a1a761295 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 12 Aug 2016 12:23:47 +0200 Subject: Fix test failures --- app/models/ci/pipeline.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 92fae78fe4e..6820b2d41a7 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -21,7 +21,7 @@ module Ci state_machine :status, initial: :created do event :queue do - transition :created => :pending + transition created: :pending transition any - [:created, :pending] => :running end -- cgit v1.2.3 From a391652fe60930e2139ecfacb175da6aa0f3b1e9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 12 Aug 2016 12:23:47 +0200 Subject: Fix test failures --- app/models/ci/pipeline.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index ad836bbebb8..98185ecd447 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -23,7 +23,7 @@ module Ci state_machine :status, initial: :created do event :queue do - transition :created => :pending + transition created: :pending transition any - [:created, :pending] => :running end -- cgit v1.2.3 From ea4ac578534d3a233c3525bf8351eb2045f6e632 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 12 Aug 2016 13:57:58 +0200 Subject: Use event `enqueue` instead of `queue` --- app/models/ci/build.rb | 4 ++-- app/models/ci/pipeline.rb | 20 +++++++------------- app/models/commit_status.rb | 2 +- app/services/ci/process_pipeline_service.rb | 2 +- 4 files changed, 11 insertions(+), 17 deletions(-) (limited to 'app') diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 92dad9377c9..3d6c6ea3209 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -59,7 +59,7 @@ module Ci when: build.when, user: user, environment: build.environment, - status_event: 'queue' + status_event: 'enqueue' ) MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build) new_build @@ -102,7 +102,7 @@ module Ci def play(current_user = nil) # Try to queue a current build - if self.queue + if self.enqueue self.update(user: current_user) self else diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 6820b2d41a7..d00de56bf07 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -20,7 +20,7 @@ module Ci after_save :keep_around_commits state_machine :status, initial: :created do - event :queue do + event :enqueue do transition created: :pending transition any - [:created, :pending] => :running end @@ -224,18 +224,12 @@ module Ci def build_updated case latest_builds_status - when 'pending' - queue - when 'running' - run - when 'success' - succeed - when 'failed' - drop - when 'canceled' - cancel - when 'skipped' - skip + when 'pending' then enqueue + when 'running' then run + when 'success' then succeed + when 'failed' then drop + when 'canceled' then cancel + when 'skipped' then skip end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index c21c8ce18db..703ca90edb6 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -26,7 +26,7 @@ class CommitStatus < ActiveRecord::Base scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) } state_machine :status do - event :queue do + event :enqueue do transition [:created, :skipped] => :pending end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 86c4823d18a..6f7610d42ba 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -37,7 +37,7 @@ module Ci return false unless Statuseable::COMPLETED_STATUSES.include?(current_status) if valid_statuses_for_when(build.when).include?(current_status) - build.queue + build.enqueue true else build.skip -- cgit v1.2.3 From a7f84d1a03978243c4fd5b8a878a4fea2b246f87 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 12 Aug 2016 13:59:20 +0200 Subject: Improve transition between states for event `enqueue` --- app/models/ci/pipeline.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index d00de56bf07..8cfba92ae9b 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -22,7 +22,7 @@ module Ci state_machine :status, initial: :created do event :enqueue do transition created: :pending - transition any - [:created, :pending] => :running + transition [:success, :failed, :canceled, :skipped] => :running end event :run do -- cgit v1.2.3 From cae0fa7cba98cb39f70deb87fe30bdc9bfa487fc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 12 Aug 2016 16:22:48 +0200 Subject: Properly select a list of Pipelines for a Merge Requests --- app/controllers/projects/merge_requests_controller.rb | 2 +- app/models/merge_request.rb | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index acdb5ba5c4f..d6128b3dfe9 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -137,7 +137,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def pipelines - @pipelines = Ci::Pipeline.where(ref: @merge_request.source_branch) + @pipelines = @merge_request.all_pipelines respond_to do |format| format.html do diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 471e32f3b60..dc758a45bcf 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -642,10 +642,21 @@ class MergeRequest < ActiveRecord::Base diverged_commits_count > 0 end + def commits_sha + commits.map(&:sha) + end + def pipeline @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project end + def all_pipelines + @all_pipelines ||= + if diff_head_sha && source_project + source_project.pipelines.order(id: :desc).where(sha: commits_sha, ref: source_branch) + end + end + def merge_commit @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha end -- cgit v1.2.3 From 1f2253545ba7a902212bace29f144a2246eeedab Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Fri, 8 Jul 2016 18:42:47 +0200 Subject: Use cache for todos counter calling TodoService --- app/models/user.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/models/user.rb b/app/models/user.rb index 73368be7b1b..87a2d999843 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -809,13 +809,13 @@ class User < ActiveRecord::Base def todos_done_count(force: false) Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do - todos.done.count + TodosFinder.new(self, state: :done).execute.count end end def todos_pending_count(force: false) Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do - todos.pending.count + TodosFinder.new(self, state: :pending).execute.count end end -- cgit v1.2.3 From f8b53ba20b74181a46985b0c7dde742239bd54f8 Mon Sep 17 00:00:00 2001 From: Paco Guzman Date: Thu, 11 Aug 2016 18:39:50 +0200 Subject: Recover usage of Todos counter cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We’re being kept up to date the counter data but we’re not using it. The only thing which is not real if is the number of projects that the user read changes the number of todos can be stale for some time. The counters will be sync just after the user receives a new todo or mark any as done --- app/controllers/dashboard/todos_controller.rb | 4 ++-- app/helpers/todos_helper.rb | 4 ++-- app/services/todo_service.rb | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) (limited to 'app') diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index 19a76a5b5d8..1243bb96d4d 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -37,8 +37,8 @@ class Dashboard::TodosController < Dashboard::ApplicationController def todos_counts { - count: TodosFinder.new(current_user, state: :pending).execute.count, - done_count: TodosFinder.new(current_user, state: :done).execute.count + count: current_user.todos_pending_count, + done_count: current_user.todos_done_count } end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index e3a208f826a..0465327060e 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -1,10 +1,10 @@ module TodosHelper def todos_pending_count - @todos_pending_count ||= TodosFinder.new(current_user, state: :pending).execute.count + @todos_pending_count ||= current_user.todos_pending_count end def todos_done_count - @todos_done_count ||= TodosFinder.new(current_user, state: :done).execute.count + @todos_done_count ||= current_user.todos_done_count end def todo_action_name(todo) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 6b48d68cccb..eb833dd82ac 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -144,8 +144,9 @@ class TodoService def mark_todos_as_done(todos, current_user) todos = current_user.todos.where(id: todos.map(&:id)) unless todos.respond_to?(:update_all) - todos.update_all(state: :done) + marked_todos = todos.update_all(state: :done) current_user.update_todos_count_cache + marked_todos end # When user marks an issue as todo -- cgit v1.2.3 From 5822a333d4c1bb43304c781f3a8b8d3eb99861a8 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Fri, 12 Aug 2016 22:09:26 +0200 Subject: Capitalise URL on web_hooks/form --- app/views/shared/web_hooks/_form.html.haml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index a672c28de39..d2ec6c3ddef 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -29,56 +29,56 @@ = f.label :push_events, class: 'list-label' do %strong Push events %p.light - This url will be triggered by a push to the repository + This URL will be triggered by a push to the repository %li = f.check_box :tag_push_events, class: 'pull-left' .prepend-left-20 = f.label :tag_push_events, class: 'list-label' do %strong Tag push events %p.light - This url will be triggered when a new tag is pushed to the repository + This URL will be triggered when a new tag is pushed to the repository %li = f.check_box :note_events, class: 'pull-left' .prepend-left-20 = f.label :note_events, class: 'list-label' do %strong Comments %p.light - This url will be triggered when someone adds a comment + This URL will be triggered when someone adds a comment %li = f.check_box :issues_events, class: 'pull-left' .prepend-left-20 = f.label :issues_events, class: 'list-label' do %strong Issues events %p.light - This url will be triggered when an issue is created/updated/merged + This URL will be triggered when an issue is created/updated/merged %li = f.check_box :merge_requests_events, class: 'pull-left' .prepend-left-20 = f.label :merge_requests_events, class: 'list-label' do %strong Merge Request events %p.light - This url will be triggered when a merge request is created/updated/merged + This URL will be triggered when a merge request is created/updated/merged %li = f.check_box :build_events, class: 'pull-left' .prepend-left-20 = f.label :build_events, class: 'list-label' do %strong Build events %p.light - This url will be triggered when the build status changes + This URL will be triggered when the build status changes %li = f.check_box :pipeline_events, class: 'pull-left' .prepend-left-20 = f.label :pipeline_events, class: 'list-label' do %strong Pipeline events %p.light - This url will be triggered when the pipeline status changes + This URL will be triggered when the pipeline status changes %li = f.check_box :wiki_page_events, class: 'pull-left' .prepend-left-20 = f.label :wiki_page_events, class: 'list-label' do %strong Wiki Page events %p.light - This url will be triggered when a wiki page is created/updated + This URL will be triggered when a wiki page is created/updated .form-group = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox' .checkbox -- cgit v1.2.3 From 1db31dbe4660ed893f1bf4eb2545b1c52ef0725b Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 11 Jul 2016 19:14:29 +0300 Subject: Added vue.js as a lib. --- app/assets/javascripts/lib/vue.js | 10073 ++++++++++++++++++++++++++++++++++++ 1 file changed, 10073 insertions(+) create mode 100644 app/assets/javascripts/lib/vue.js (limited to 'app') diff --git a/app/assets/javascripts/lib/vue.js b/app/assets/javascripts/lib/vue.js new file mode 100644 index 00000000000..efc62983910 --- /dev/null +++ b/app/assets/javascripts/lib/vue.js @@ -0,0 +1,10073 @@ +/*! + * Vue.js v1.0.26 + * (c) 2016 Evan You + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.Vue = factory()); +}(this, function () { 'use strict'; + + function set(obj, key, val) { + if (hasOwn(obj, key)) { + obj[key] = val; + return; + } + if (obj._isVue) { + set(obj._data, key, val); + return; + } + var ob = obj.__ob__; + if (!ob) { + obj[key] = val; + return; + } + ob.convert(key, val); + ob.dep.notify(); + if (ob.vms) { + var i = ob.vms.length; + while (i--) { + var vm = ob.vms[i]; + vm._proxy(key); + vm._digest(); + } + } + return val; + } + + /** + * Delete a property and trigger change if necessary. + * + * @param {Object} obj + * @param {String} key + */ + + function del(obj, key) { + if (!hasOwn(obj, key)) { + return; + } + delete obj[key]; + var ob = obj.__ob__; + if (!ob) { + if (obj._isVue) { + delete obj._data[key]; + obj._digest(); + } + return; + } + ob.dep.notify(); + if (ob.vms) { + var i = ob.vms.length; + while (i--) { + var vm = ob.vms[i]; + vm._unproxy(key); + vm._digest(); + } + } + } + + var hasOwnProperty = Object.prototype.hasOwnProperty; + /** + * Check whether the object has the property. + * + * @param {Object} obj + * @param {String} key + * @return {Boolean} + */ + + function hasOwn(obj, key) { + return hasOwnProperty.call(obj, key); + } + + /** + * Check if an expression is a literal value. + * + * @param {String} exp + * @return {Boolean} + */ + + var literalValueRE = /^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/; + + function isLiteral(exp) { + return literalValueRE.test(exp); + } + + /** + * Check if a string starts with $ or _ + * + * @param {String} str + * @return {Boolean} + */ + + function isReserved(str) { + var c = (str + '').charCodeAt(0); + return c === 0x24 || c === 0x5F; + } + + /** + * Guard text output, make sure undefined outputs + * empty string + * + * @param {*} value + * @return {String} + */ + + function _toString(value) { + return value == null ? '' : value.toString(); + } + + /** + * Check and convert possible numeric strings to numbers + * before setting back to data + * + * @param {*} value + * @return {*|Number} + */ + + function toNumber(value) { + if (typeof value !== 'string') { + return value; + } else { + var parsed = Number(value); + return isNaN(parsed) ? value : parsed; + } + } + + /** + * Convert string boolean literals into real booleans. + * + * @param {*} value + * @return {*|Boolean} + */ + + function toBoolean(value) { + return value === 'true' ? true : value === 'false' ? false : value; + } + + /** + * Strip quotes from a string + * + * @param {String} str + * @return {String | false} + */ + + function stripQuotes(str) { + var a = str.charCodeAt(0); + var b = str.charCodeAt(str.length - 1); + return a === b && (a === 0x22 || a === 0x27) ? str.slice(1, -1) : str; + } + + /** + * Camelize a hyphen-delmited string. + * + * @param {String} str + * @return {String} + */ + + var camelizeRE = /-(\w)/g; + + function camelize(str) { + return str.replace(camelizeRE, toUpper); + } + + function toUpper(_, c) { + return c ? c.toUpperCase() : ''; + } + + /** + * Hyphenate a camelCase string. + * + * @param {String} str + * @return {String} + */ + + var hyphenateRE = /([a-z\d])([A-Z])/g; + + function hyphenate(str) { + return str.replace(hyphenateRE, '$1-$2').toLowerCase(); + } + + /** + * Converts hyphen/underscore/slash delimitered names into + * camelized classNames. + * + * e.g. my-component => MyComponent + * some_else => SomeElse + * some/comp => SomeComp + * + * @param {String} str + * @return {String} + */ + + var classifyRE = /(?:^|[-_\/])(\w)/g; + + function classify(str) { + return str.replace(classifyRE, toUpper); + } + + /** + * Simple bind, faster than native + * + * @param {Function} fn + * @param {Object} ctx + * @return {Function} + */ + + function bind(fn, ctx) { + return function (a) { + var l = arguments.length; + return l ? l > 1 ? fn.apply(ctx, arguments) : fn.call(ctx, a) : fn.call(ctx); + }; + } + + /** + * Convert an Array-like object to a real Array. + * + * @param {Array-like} list + * @param {Number} [start] - start index + * @return {Array} + */ + + function toArray(list, start) { + start = start || 0; + var i = list.length - start; + var ret = new Array(i); + while (i--) { + ret[i] = list[i + start]; + } + return ret; + } + + /** + * Mix properties into target object. + * + * @param {Object} to + * @param {Object} from + */ + + function extend(to, from) { + var keys = Object.keys(from); + var i = keys.length; + while (i--) { + to[keys[i]] = from[keys[i]]; + } + return to; + } + + /** + * Quick object check - this is primarily used to tell + * Objects from primitive values when we know the value + * is a JSON-compliant type. + * + * @param {*} obj + * @return {Boolean} + */ + + function isObject(obj) { + return obj !== null && typeof obj === 'object'; + } + + /** + * Strict object type check. Only returns true + * for plain JavaScript objects. + * + * @param {*} obj + * @return {Boolean} + */ + + var toString = Object.prototype.toString; + var OBJECT_STRING = '[object Object]'; + + function isPlainObject(obj) { + return toString.call(obj) === OBJECT_STRING; + } + + /** + * Array type check. + * + * @param {*} obj + * @return {Boolean} + */ + + var isArray = Array.isArray; + + /** + * Define a property. + * + * @param {Object} obj + * @param {String} key + * @param {*} val + * @param {Boolean} [enumerable] + */ + + function def(obj, key, val, enumerable) { + Object.defineProperty(obj, key, { + value: val, + enumerable: !!enumerable, + writable: true, + configurable: true + }); + } + + /** + * Debounce a function so it only gets called after the + * input stops arriving after the given wait period. + * + * @param {Function} func + * @param {Number} wait + * @return {Function} - the debounced function + */ + + function _debounce(func, wait) { + var timeout, args, context, timestamp, result; + var later = function later() { + var last = Date.now() - timestamp; + if (last < wait && last >= 0) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + } + }; + return function () { + context = this; + args = arguments; + timestamp = Date.now(); + if (!timeout) { + timeout = setTimeout(later, wait); + } + return result; + }; + } + + /** + * Manual indexOf because it's slightly faster than + * native. + * + * @param {Array} arr + * @param {*} obj + */ + + function indexOf(arr, obj) { + var i = arr.length; + while (i--) { + if (arr[i] === obj) return i; + } + return -1; + } + + /** + * Make a cancellable version of an async callback. + * + * @param {Function} fn + * @return {Function} + */ + + function cancellable(fn) { + var cb = function cb() { + if (!cb.cancelled) { + return fn.apply(this, arguments); + } + }; + cb.cancel = function () { + cb.cancelled = true; + }; + return cb; + } + + /** + * Check if two values are loosely equal - that is, + * if they are plain objects, do they have the same shape? + * + * @param {*} a + * @param {*} b + * @return {Boolean} + */ + + function looseEqual(a, b) { + /* eslint-disable eqeqeq */ + return a == b || (isObject(a) && isObject(b) ? JSON.stringify(a) === JSON.stringify(b) : false); + /* eslint-enable eqeqeq */ + } + + var hasProto = ('__proto__' in {}); + + // Browser environment sniffing + var inBrowser = typeof window !== 'undefined' && Object.prototype.toString.call(window) !== '[object Object]'; + + // detect devtools + var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; + + // UA sniffing for working around browser-specific quirks + var UA = inBrowser && window.navigator.userAgent.toLowerCase(); + var isIE = UA && UA.indexOf('trident') > 0; + var isIE9 = UA && UA.indexOf('msie 9.0') > 0; + var isAndroid = UA && UA.indexOf('android') > 0; + var isIos = UA && /(iphone|ipad|ipod|ios)/i.test(UA); + var iosVersionMatch = isIos && UA.match(/os ([\d_]+)/); + var iosVersion = iosVersionMatch && iosVersionMatch[1].split('_'); + + // detecting iOS UIWebView by indexedDB + var hasMutationObserverBug = iosVersion && Number(iosVersion[0]) >= 9 && Number(iosVersion[1]) >= 3 && !window.indexedDB; + + var transitionProp = undefined; + var transitionEndEvent = undefined; + var animationProp = undefined; + var animationEndEvent = undefined; + + // Transition property/event sniffing + if (inBrowser && !isIE9) { + var isWebkitTrans = window.ontransitionend === undefined && window.onwebkittransitionend !== undefined; + var isWebkitAnim = window.onanimationend === undefined && window.onwebkitanimationend !== undefined; + transitionProp = isWebkitTrans ? 'WebkitTransition' : 'transition'; + transitionEndEvent = isWebkitTrans ? 'webkitTransitionEnd' : 'transitionend'; + animationProp = isWebkitAnim ? 'WebkitAnimation' : 'animation'; + animationEndEvent = isWebkitAnim ? 'webkitAnimationEnd' : 'animationend'; + } + + /** + * Defer a task to execute it asynchronously. Ideally this + * should be executed as a microtask, so we leverage + * MutationObserver if it's available, and fallback to + * setTimeout(0). + * + * @param {Function} cb + * @param {Object} ctx + */ + + var nextTick = (function () { + var callbacks = []; + var pending = false; + var timerFunc; + function nextTickHandler() { + pending = false; + var copies = callbacks.slice(0); + callbacks = []; + for (var i = 0; i < copies.length; i++) { + copies[i](); + } + } + + /* istanbul ignore if */ + if (typeof MutationObserver !== 'undefined' && !hasMutationObserverBug) { + var counter = 1; + var observer = new MutationObserver(nextTickHandler); + var textNode = document.createTextNode(counter); + observer.observe(textNode, { + characterData: true + }); + timerFunc = function () { + counter = (counter + 1) % 2; + textNode.data = counter; + }; + } else { + // webpack attempts to inject a shim for setImmediate + // if it is used as a global, so we have to work around that to + // avoid bundling unnecessary code. + var context = inBrowser ? window : typeof global !== 'undefined' ? global : {}; + timerFunc = context.setImmediate || setTimeout; + } + return function (cb, ctx) { + var func = ctx ? function () { + cb.call(ctx); + } : cb; + callbacks.push(func); + if (pending) return; + pending = true; + timerFunc(nextTickHandler, 0); + }; + })(); + + var _Set = undefined; + /* istanbul ignore if */ + if (typeof Set !== 'undefined' && Set.toString().match(/native code/)) { + // use native Set when available. + _Set = Set; + } else { + // a non-standard Set polyfill that only works with primitive keys. + _Set = function () { + this.set = Object.create(null); + }; + _Set.prototype.has = function (key) { + return this.set[key] !== undefined; + }; + _Set.prototype.add = function (key) { + this.set[key] = 1; + }; + _Set.prototype.clear = function () { + this.set = Object.create(null); + }; + } + + function Cache(limit) { + this.size = 0; + this.limit = limit; + this.head = this.tail = undefined; + this._keymap = Object.create(null); + } + + var p = Cache.prototype; + + /** + * Put into the cache associated with . + * Returns the entry which was removed to make room for + * the new entry. Otherwise undefined is returned. + * (i.e. if there was enough room already). + * + * @param {String} key + * @param {*} value + * @return {Entry|undefined} + */ + + p.put = function (key, value) { + var removed; + + var entry = this.get(key, true); + if (!entry) { + if (this.size === this.limit) { + removed = this.shift(); + } + entry = { + key: key + }; + this._keymap[key] = entry; + if (this.tail) { + this.tail.newer = entry; + entry.older = this.tail; + } else { + this.head = entry; + } + this.tail = entry; + this.size++; + } + entry.value = value; + + return removed; + }; + + /** + * Purge the least recently used (oldest) entry from the + * cache. Returns the removed entry or undefined if the + * cache was empty. + */ + + p.shift = function () { + var entry = this.head; + if (entry) { + this.head = this.head.newer; + this.head.older = undefined; + entry.newer = entry.older = undefined; + this._keymap[entry.key] = undefined; + this.size--; + } + return entry; + }; + + /** + * Get and register recent use of . Returns the value + * associated with or undefined if not in cache. + * + * @param {String} key + * @param {Boolean} returnEntry + * @return {Entry|*} + */ + + p.get = function (key, returnEntry) { + var entry = this._keymap[key]; + if (entry === undefined) return; + if (entry === this.tail) { + return returnEntry ? entry : entry.value; + } + // HEAD--------------TAIL + // <.older .newer> + // <--- add direction -- + // A B C E + if (entry.newer) { + if (entry === this.head) { + this.head = entry.newer; + } + entry.newer.older = entry.older; // C <-- E. + } + if (entry.older) { + entry.older.newer = entry.newer; // C. --> E + } + entry.newer = undefined; // D --x + entry.older = this.tail; // D. --> E + if (this.tail) { + this.tail.newer = entry; // E. <-- D + } + this.tail = entry; + return returnEntry ? entry : entry.value; + }; + + var cache$1 = new Cache(1000); + var filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g; + var reservedArgRE = /^in$|^-?\d+/; + + /** + * Parser state + */ + + var str; + var dir; + var c; + var prev; + var i; + var l; + var lastFilterIndex; + var inSingle; + var inDouble; + var curly; + var square; + var paren; + /** + * Push a filter to the current directive object + */ + + function pushFilter() { + var exp = str.slice(lastFilterIndex, i).trim(); + var filter; + if (exp) { + filter = {}; + var tokens = exp.match(filterTokenRE); + filter.name = tokens[0]; + if (tokens.length > 1) { + filter.args = tokens.slice(1).map(processFilterArg); + } + } + if (filter) { + (dir.filters = dir.filters || []).push(filter); + } + lastFilterIndex = i + 1; + } + + /** + * Check if an argument is dynamic and strip quotes. + * + * @param {String} arg + * @return {Object} + */ + + function processFilterArg(arg) { + if (reservedArgRE.test(arg)) { + return { + value: toNumber(arg), + dynamic: false + }; + } else { + var stripped = stripQuotes(arg); + var dynamic = stripped === arg; + return { + value: dynamic ? arg : stripped, + dynamic: dynamic + }; + } + } + + /** + * Parse a directive value and extract the expression + * and its filters into a descriptor. + * + * Example: + * + * "a + 1 | uppercase" will yield: + * { + * expression: 'a + 1', + * filters: [ + * { name: 'uppercase', args: null } + * ] + * } + * + * @param {String} s + * @return {Object} + */ + + function parseDirective(s) { + var hit = cache$1.get(s); + if (hit) { + return hit; + } + + // reset parser state + str = s; + inSingle = inDouble = false; + curly = square = paren = 0; + lastFilterIndex = 0; + dir = {}; + + for (i = 0, l = str.length; i < l; i++) { + prev = c; + c = str.charCodeAt(i); + if (inSingle) { + // check single quote + if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle; + } else if (inDouble) { + // check double quote + if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble; + } else if (c === 0x7C && // pipe + str.charCodeAt(i + 1) !== 0x7C && str.charCodeAt(i - 1) !== 0x7C) { + if (dir.expression == null) { + // first filter, end of expression + lastFilterIndex = i + 1; + dir.expression = str.slice(0, i).trim(); + } else { + // already has filter + pushFilter(); + } + } else { + switch (c) { + case 0x22: + inDouble = true;break; // " + case 0x27: + inSingle = true;break; // ' + case 0x28: + paren++;break; // ( + case 0x29: + paren--;break; // ) + case 0x5B: + square++;break; // [ + case 0x5D: + square--;break; // ] + case 0x7B: + curly++;break; // { + case 0x7D: + curly--;break; // } + } + } + } + + if (dir.expression == null) { + dir.expression = str.slice(0, i).trim(); + } else if (lastFilterIndex !== 0) { + pushFilter(); + } + + cache$1.put(s, dir); + return dir; + } + +var directive = Object.freeze({ + parseDirective: parseDirective + }); + + var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g; + var cache = undefined; + var tagRE = undefined; + var htmlRE = undefined; + /** + * Escape a string so it can be used in a RegExp + * constructor. + * + * @param {String} str + */ + + function escapeRegex(str) { + return str.replace(regexEscapeRE, '\\$&'); + } + + function compileRegex() { + var open = escapeRegex(config.delimiters[0]); + var close = escapeRegex(config.delimiters[1]); + var unsafeOpen = escapeRegex(config.unsafeDelimiters[0]); + var unsafeClose = escapeRegex(config.unsafeDelimiters[1]); + tagRE = new RegExp(unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '|' + open + '((?:.|\\n)+?)' + close, 'g'); + htmlRE = new RegExp('^' + unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '$'); + // reset cache + cache = new Cache(1000); + } + + /** + * Parse a template text string into an array of tokens. + * + * @param {String} text + * @return {Array | null} + * - {String} type + * - {String} value + * - {Boolean} [html] + * - {Boolean} [oneTime] + */ + + function parseText(text) { + if (!cache) { + compileRegex(); + } + var hit = cache.get(text); + if (hit) { + return hit; + } + if (!tagRE.test(text)) { + return null; + } + var tokens = []; + var lastIndex = tagRE.lastIndex = 0; + var match, index, html, value, first, oneTime; + /* eslint-disable no-cond-assign */ + while (match = tagRE.exec(text)) { + /* eslint-enable no-cond-assign */ + index = match.index; + // push text token + if (index > lastIndex) { + tokens.push({ + value: text.slice(lastIndex, index) + }); + } + // tag token + html = htmlRE.test(match[0]); + value = html ? match[1] : match[2]; + first = value.charCodeAt(0); + oneTime = first === 42; // * + value = oneTime ? value.slice(1) : value; + tokens.push({ + tag: true, + value: value.trim(), + html: html, + oneTime: oneTime + }); + lastIndex = index + match[0].length; + } + if (lastIndex < text.length) { + tokens.push({ + value: text.slice(lastIndex) + }); + } + cache.put(text, tokens); + return tokens; + } + + /** + * Format a list of tokens into an expression. + * e.g. tokens parsed from 'a {{b}} c' can be serialized + * into one single expression as '"a " + b + " c"'. + * + * @param {Array} tokens + * @param {Vue} [vm] + * @return {String} + */ + + function tokensToExp(tokens, vm) { + if (tokens.length > 1) { + return tokens.map(function (token) { + return formatToken(token, vm); + }).join('+'); + } else { + return formatToken(tokens[0], vm, true); + } + } + + /** + * Format a single token. + * + * @param {Object} token + * @param {Vue} [vm] + * @param {Boolean} [single] + * @return {String} + */ + + function formatToken(token, vm, single) { + return token.tag ? token.oneTime && vm ? '"' + vm.$eval(token.value) + '"' : inlineFilters(token.value, single) : '"' + token.value + '"'; + } + + /** + * For an attribute with multiple interpolation tags, + * e.g. attr="some-{{thing | filter}}", in order to combine + * the whole thing into a single watchable expression, we + * have to inline those filters. This function does exactly + * that. This is a bit hacky but it avoids heavy changes + * to directive parser and watcher mechanism. + * + * @param {String} exp + * @param {Boolean} single + * @return {String} + */ + + var filterRE = /[^|]\|[^|]/; + function inlineFilters(exp, single) { + if (!filterRE.test(exp)) { + return single ? exp : '(' + exp + ')'; + } else { + var dir = parseDirective(exp); + if (!dir.filters) { + return '(' + exp + ')'; + } else { + return 'this._applyFilters(' + dir.expression + // value + ',null,' + // oldValue (null for read) + JSON.stringify(dir.filters) + // filter descriptors + ',false)'; // write? + } + } + } + +var text = Object.freeze({ + compileRegex: compileRegex, + parseText: parseText, + tokensToExp: tokensToExp + }); + + var delimiters = ['{{', '}}']; + var unsafeDelimiters = ['{{{', '}}}']; + + var config = Object.defineProperties({ + + /** + * Whether to print debug messages. + * Also enables stack trace for warnings. + * + * @type {Boolean} + */ + + debug: false, + + /** + * Whether to suppress warnings. + * + * @type {Boolean} + */ + + silent: false, + + /** + * Whether to use async rendering. + */ + + async: true, + + /** + * Whether to warn against errors caught when evaluating + * expressions. + */ + + warnExpressionErrors: true, + + /** + * Whether to allow devtools inspection. + * Disabled by default in production builds. + */ + + devtools: 'development' !== 'production', + + /** + * Internal flag to indicate the delimiters have been + * changed. + * + * @type {Boolean} + */ + + _delimitersChanged: true, + + /** + * List of asset types that a component can own. + * + * @type {Array} + */ + + _assetTypes: ['component', 'directive', 'elementDirective', 'filter', 'transition', 'partial'], + + /** + * prop binding modes + */ + + _propBindingModes: { + ONE_WAY: 0, + TWO_WAY: 1, + ONE_TIME: 2 + }, + + /** + * Max circular updates allowed in a batcher flush cycle. + */ + + _maxUpdateCount: 100 + + }, { + delimiters: { /** + * Interpolation delimiters. Changing these would trigger + * the text parser to re-compile the regular expressions. + * + * @type {Array} + */ + + get: function get() { + return delimiters; + }, + set: function set(val) { + delimiters = val; + compileRegex(); + }, + configurable: true, + enumerable: true + }, + unsafeDelimiters: { + get: function get() { + return unsafeDelimiters; + }, + set: function set(val) { + unsafeDelimiters = val; + compileRegex(); + }, + configurable: true, + enumerable: true + } + }); + + var warn = undefined; + var formatComponentName = undefined; + + if ('development' !== 'production') { + (function () { + var hasConsole = typeof console !== 'undefined'; + + warn = function (msg, vm) { + if (hasConsole && !config.silent) { + console.error('[Vue warn]: ' + msg + (vm ? formatComponentName(vm) : '')); + } + }; + + formatComponentName = function (vm) { + var name = vm._isVue ? vm.$options.name : vm.name; + return name ? ' (found in component: <' + hyphenate(name) + '>)' : ''; + }; + })(); + } + + /** + * Append with transition. + * + * @param {Element} el + * @param {Element} target + * @param {Vue} vm + * @param {Function} [cb] + */ + + function appendWithTransition(el, target, vm, cb) { + applyTransition(el, 1, function () { + target.appendChild(el); + }, vm, cb); + } + + /** + * InsertBefore with transition. + * + * @param {Element} el + * @param {Element} target + * @param {Vue} vm + * @param {Function} [cb] + */ + + function beforeWithTransition(el, target, vm, cb) { + applyTransition(el, 1, function () { + before(el, target); + }, vm, cb); + } + + /** + * Remove with transition. + * + * @param {Element} el + * @param {Vue} vm + * @param {Function} [cb] + */ + + function removeWithTransition(el, vm, cb) { + applyTransition(el, -1, function () { + remove(el); + }, vm, cb); + } + + /** + * Apply transitions with an operation callback. + * + * @param {Element} el + * @param {Number} direction + * 1: enter + * -1: leave + * @param {Function} op - the actual DOM operation + * @param {Vue} vm + * @param {Function} [cb] + */ + + function applyTransition(el, direction, op, vm, cb) { + var transition = el.__v_trans; + if (!transition || + // skip if there are no js hooks and CSS transition is + // not supported + !transition.hooks && !transitionEndEvent || + // skip transitions for initial compile + !vm._isCompiled || + // if the vm is being manipulated by a parent directive + // during the parent's compilation phase, skip the + // animation. + vm.$parent && !vm.$parent._isCompiled) { + op(); + if (cb) cb(); + return; + } + var action = direction > 0 ? 'enter' : 'leave'; + transition[action](op, cb); + } + +var transition = Object.freeze({ + appendWithTransition: appendWithTransition, + beforeWithTransition: beforeWithTransition, + removeWithTransition: removeWithTransition, + applyTransition: applyTransition + }); + + /** + * Query an element selector if it's not an element already. + * + * @param {String|Element} el + * @return {Element} + */ + + function query(el) { + if (typeof el === 'string') { + var selector = el; + el = document.querySelector(el); + if (!el) { + 'development' !== 'production' && warn('Cannot find element: ' + selector); + } + } + return el; + } + + /** + * Check if a node is in the document. + * Note: document.documentElement.contains should work here + * but always returns false for comment nodes in phantomjs, + * making unit tests difficult. This is fixed by doing the + * contains() check on the node's parentNode instead of + * the node itself. + * + * @param {Node} node + * @return {Boolean} + */ + + function inDoc(node) { + if (!node) return false; + var doc = node.ownerDocument.documentElement; + var parent = node.parentNode; + return doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent)); + } + + /** + * Get and remove an attribute from a node. + * + * @param {Node} node + * @param {String} _attr + */ + + function getAttr(node, _attr) { + var val = node.getAttribute(_attr); + if (val !== null) { + node.removeAttribute(_attr); + } + return val; + } + + /** + * Get an attribute with colon or v-bind: prefix. + * + * @param {Node} node + * @param {String} name + * @return {String|null} + */ + + function getBindAttr(node, name) { + var val = getAttr(node, ':' + name); + if (val === null) { + val = getAttr(node, 'v-bind:' + name); + } + return val; + } + + /** + * Check the presence of a bind attribute. + * + * @param {Node} node + * @param {String} name + * @return {Boolean} + */ + + function hasBindAttr(node, name) { + return node.hasAttribute(name) || node.hasAttribute(':' + name) || node.hasAttribute('v-bind:' + name); + } + + /** + * Insert el before target + * + * @param {Element} el + * @param {Element} target + */ + + function before(el, target) { + target.parentNode.insertBefore(el, target); + } + + /** + * Insert el after target + * + * @param {Element} el + * @param {Element} target + */ + + function after(el, target) { + if (target.nextSibling) { + before(el, target.nextSibling); + } else { + target.parentNode.appendChild(el); + } + } + + /** + * Remove el from DOM + * + * @param {Element} el + */ + + function remove(el) { + el.parentNode.removeChild(el); + } + + /** + * Prepend el to target + * + * @param {Element} el + * @param {Element} target + */ + + function prepend(el, target) { + if (target.firstChild) { + before(el, target.firstChild); + } else { + target.appendChild(el); + } + } + + /** + * Replace target with el + * + * @param {Element} target + * @param {Element} el + */ + + function replace(target, el) { + var parent = target.parentNode; + if (parent) { + parent.replaceChild(el, target); + } + } + + /** + * Add event listener shorthand. + * + * @param {Element} el + * @param {String} event + * @param {Function} cb + * @param {Boolean} [useCapture] + */ + + function on(el, event, cb, useCapture) { + el.addEventListener(event, cb, useCapture); + } + + /** + * Remove event listener shorthand. + * + * @param {Element} el + * @param {String} event + * @param {Function} cb + */ + + function off(el, event, cb) { + el.removeEventListener(event, cb); + } + + /** + * For IE9 compat: when both class and :class are present + * getAttribute('class') returns wrong value... + * + * @param {Element} el + * @return {String} + */ + + function getClass(el) { + var classname = el.className; + if (typeof classname === 'object') { + classname = classname.baseVal || ''; + } + return classname; + } + + /** + * In IE9, setAttribute('class') will result in empty class + * if the element also has the :class attribute; However in + * PhantomJS, setting `className` does not work on SVG elements... + * So we have to do a conditional check here. + * + * @param {Element} el + * @param {String} cls + */ + + function setClass(el, cls) { + /* istanbul ignore if */ + if (isIE9 && !/svg$/.test(el.namespaceURI)) { + el.className = cls; + } else { + el.setAttribute('class', cls); + } + } + + /** + * Add class with compatibility for IE & SVG + * + * @param {Element} el + * @param {String} cls + */ + + function addClass(el, cls) { + if (el.classList) { + el.classList.add(cls); + } else { + var cur = ' ' + getClass(el) + ' '; + if (cur.indexOf(' ' + cls + ' ') < 0) { + setClass(el, (cur + cls).trim()); + } + } + } + + /** + * Remove class with compatibility for IE & SVG + * + * @param {Element} el + * @param {String} cls + */ + + function removeClass(el, cls) { + if (el.classList) { + el.classList.remove(cls); + } else { + var cur = ' ' + getClass(el) + ' '; + var tar = ' ' + cls + ' '; + while (cur.indexOf(tar) >= 0) { + cur = cur.replace(tar, ' '); + } + setClass(el, cur.trim()); + } + if (!el.className) { + el.removeAttribute('class'); + } + } + + /** + * Extract raw content inside an element into a temporary + * container div + * + * @param {Element} el + * @param {Boolean} asFragment + * @return {Element|DocumentFragment} + */ + + function extractContent(el, asFragment) { + var child; + var rawContent; + /* istanbul ignore if */ + if (isTemplate(el) && isFragment(el.content)) { + el = el.content; + } + if (el.hasChildNodes()) { + trimNode(el); + rawContent = asFragment ? document.createDocumentFragment() : document.createElement('div'); + /* eslint-disable no-cond-assign */ + while (child = el.firstChild) { + /* eslint-enable no-cond-assign */ + rawContent.appendChild(child); + } + } + return rawContent; + } + + /** + * Trim possible empty head/tail text and comment + * nodes inside a parent. + * + * @param {Node} node + */ + + function trimNode(node) { + var child; + /* eslint-disable no-sequences */ + while ((child = node.firstChild, isTrimmable(child))) { + node.removeChild(child); + } + while ((child = node.lastChild, isTrimmable(child))) { + node.removeChild(child); + } + /* eslint-enable no-sequences */ + } + + function isTrimmable(node) { + return node && (node.nodeType === 3 && !node.data.trim() || node.nodeType === 8); + } + + /** + * Check if an element is a template tag. + * Note if the template appears inside an SVG its tagName + * will be in lowercase. + * + * @param {Element} el + */ + + function isTemplate(el) { + return el.tagName && el.tagName.toLowerCase() === 'template'; + } + + /** + * Create an "anchor" for performing dom insertion/removals. + * This is used in a number of scenarios: + * - fragment instance + * - v-html + * - v-if + * - v-for + * - component + * + * @param {String} content + * @param {Boolean} persist - IE trashes empty textNodes on + * cloneNode(true), so in certain + * cases the anchor needs to be + * non-empty to be persisted in + * templates. + * @return {Comment|Text} + */ + + function createAnchor(content, persist) { + var anchor = config.debug ? document.createComment(content) : document.createTextNode(persist ? ' ' : ''); + anchor.__v_anchor = true; + return anchor; + } + + /** + * Find a component ref attribute that starts with $. + * + * @param {Element} node + * @return {String|undefined} + */ + + var refRE = /^v-ref:/; + + function findRef(node) { + if (node.hasAttributes()) { + var attrs = node.attributes; + for (var i = 0, l = attrs.length; i < l; i++) { + var name = attrs[i].name; + if (refRE.test(name)) { + return camelize(name.replace(refRE, '')); + } + } + } + } + + /** + * Map a function to a range of nodes . + * + * @param {Node} node + * @param {Node} end + * @param {Function} op + */ + + function mapNodeRange(node, end, op) { + var next; + while (node !== end) { + next = node.nextSibling; + op(node); + node = next; + } + op(end); + } + + /** + * Remove a range of nodes with transition, store + * the nodes in a fragment with correct ordering, + * and call callback when done. + * + * @param {Node} start + * @param {Node} end + * @param {Vue} vm + * @param {DocumentFragment} frag + * @param {Function} cb + */ + + function removeNodeRange(start, end, vm, frag, cb) { + var done = false; + var removed = 0; + var nodes = []; + mapNodeRange(start, end, function (node) { + if (node === end) done = true; + nodes.push(node); + removeWithTransition(node, vm, onRemoved); + }); + function onRemoved() { + removed++; + if (done && removed >= nodes.length) { + for (var i = 0; i < nodes.length; i++) { + frag.appendChild(nodes[i]); + } + cb && cb(); + } + } + } + + /** + * Check if a node is a DocumentFragment. + * + * @param {Node} node + * @return {Boolean} + */ + + function isFragment(node) { + return node && node.nodeType === 11; + } + + /** + * Get outerHTML of elements, taking care + * of SVG elements in IE as well. + * + * @param {Element} el + * @return {String} + */ + + function getOuterHTML(el) { + if (el.outerHTML) { + return el.outerHTML; + } else { + var container = document.createElement('div'); + container.appendChild(el.cloneNode(true)); + return container.innerHTML; + } + } + + var commonTagRE = /^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i; + var reservedTagRE = /^(slot|partial|component)$/i; + + var isUnknownElement = undefined; + if ('development' !== 'production') { + isUnknownElement = function (el, tag) { + if (tag.indexOf('-') > -1) { + // http://stackoverflow.com/a/28210364/1070244 + return el.constructor === window.HTMLUnknownElement || el.constructor === window.HTMLElement; + } else { + return (/HTMLUnknownElement/.test(el.toString()) && + // Chrome returns unknown for several HTML5 elements. + // https://code.google.com/p/chromium/issues/detail?id=540526 + // Firefox returns unknown for some "Interactive elements." + !/^(data|time|rtc|rb|details|dialog|summary)$/.test(tag) + ); + } + }; + } + + /** + * Check if an element is a component, if yes return its + * component id. + * + * @param {Element} el + * @param {Object} options + * @return {Object|undefined} + */ + + function checkComponentAttr(el, options) { + var tag = el.tagName.toLowerCase(); + var hasAttrs = el.hasAttributes(); + if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) { + if (resolveAsset(options, 'components', tag)) { + return { id: tag }; + } else { + var is = hasAttrs && getIsBinding(el, options); + if (is) { + return is; + } else if ('development' !== 'production') { + var expectedTag = options._componentNameMap && options._componentNameMap[tag]; + if (expectedTag) { + warn('Unknown custom element: <' + tag + '> - ' + 'did you mean <' + expectedTag + '>? ' + 'HTML is case-insensitive, remember to use kebab-case in templates.'); + } else if (isUnknownElement(el, tag)) { + warn('Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.'); + } + } + } + } else if (hasAttrs) { + return getIsBinding(el, options); + } + } + + /** + * Get "is" binding from an element. + * + * @param {Element} el + * @param {Object} options + * @return {Object|undefined} + */ + + function getIsBinding(el, options) { + // dynamic syntax + var exp = el.getAttribute('is'); + if (exp != null) { + if (resolveAsset(options, 'components', exp)) { + el.removeAttribute('is'); + return { id: exp }; + } + } else { + exp = getBindAttr(el, 'is'); + if (exp != null) { + return { id: exp, dynamic: true }; + } + } + } + + /** + * Option overwriting strategies are functions that handle + * how to merge a parent option value and a child option + * value into the final value. + * + * All strategy functions follow the same signature: + * + * @param {*} parentVal + * @param {*} childVal + * @param {Vue} [vm] + */ + + var strats = config.optionMergeStrategies = Object.create(null); + + /** + * Helper that recursively merges two data objects together. + */ + + function mergeData(to, from) { + var key, toVal, fromVal; + for (key in from) { + toVal = to[key]; + fromVal = from[key]; + if (!hasOwn(to, key)) { + set(to, key, fromVal); + } else if (isObject(toVal) && isObject(fromVal)) { + mergeData(toVal, fromVal); + } + } + return to; + } + + /** + * Data + */ + + strats.data = function (parentVal, childVal, vm) { + if (!vm) { + // in a Vue.extend merge, both should be functions + if (!childVal) { + return parentVal; + } + if (typeof childVal !== 'function') { + 'development' !== 'production' && warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm); + return parentVal; + } + if (!parentVal) { + return childVal; + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + return function mergedDataFn() { + return mergeData(childVal.call(this), parentVal.call(this)); + }; + } else if (parentVal || childVal) { + return function mergedInstanceDataFn() { + // instance merge + var instanceData = typeof childVal === 'function' ? childVal.call(vm) : childVal; + var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : undefined; + if (instanceData) { + return mergeData(instanceData, defaultData); + } else { + return defaultData; + } + }; + } + }; + + /** + * El + */ + + strats.el = function (parentVal, childVal, vm) { + if (!vm && childVal && typeof childVal !== 'function') { + 'development' !== 'production' && warn('The "el" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm); + return; + } + var ret = childVal || parentVal; + // invoke the element factory if this is instance merge + return vm && typeof ret === 'function' ? ret.call(vm) : ret; + }; + + /** + * Hooks and param attributes are merged as arrays. + */ + + strats.init = strats.created = strats.ready = strats.attached = strats.detached = strats.beforeCompile = strats.compiled = strats.beforeDestroy = strats.destroyed = strats.activate = function (parentVal, childVal) { + return childVal ? parentVal ? parentVal.concat(childVal) : isArray(childVal) ? childVal : [childVal] : parentVal; + }; + + /** + * Assets + * + * When a vm is present (instance creation), we need to do + * a three-way merge between constructor options, instance + * options and parent options. + */ + + function mergeAssets(parentVal, childVal) { + var res = Object.create(parentVal || null); + return childVal ? extend(res, guardArrayAssets(childVal)) : res; + } + + config._assetTypes.forEach(function (type) { + strats[type + 's'] = mergeAssets; + }); + + /** + * Events & Watchers. + * + * Events & watchers hashes should not overwrite one + * another, so we merge them as arrays. + */ + + strats.watch = strats.events = function (parentVal, childVal) { + if (!childVal) return parentVal; + if (!parentVal) return childVal; + var ret = {}; + extend(ret, parentVal); + for (var key in childVal) { + var parent = ret[key]; + var child = childVal[key]; + if (parent && !isArray(parent)) { + parent = [parent]; + } + ret[key] = parent ? parent.concat(child) : [child]; + } + return ret; + }; + + /** + * Other object hashes. + */ + + strats.props = strats.methods = strats.computed = function (parentVal, childVal) { + if (!childVal) return parentVal; + if (!parentVal) return childVal; + var ret = Object.create(null); + extend(ret, parentVal); + extend(ret, childVal); + return ret; + }; + + /** + * Default strategy. + */ + + var defaultStrat = function defaultStrat(parentVal, childVal) { + return childVal === undefined ? parentVal : childVal; + }; + + /** + * Make sure component options get converted to actual + * constructors. + * + * @param {Object} options + */ + + function guardComponents(options) { + if (options.components) { + var components = options.components = guardArrayAssets(options.components); + var ids = Object.keys(components); + var def; + if ('development' !== 'production') { + var map = options._componentNameMap = {}; + } + for (var i = 0, l = ids.length; i < l; i++) { + var key = ids[i]; + if (commonTagRE.test(key) || reservedTagRE.test(key)) { + 'development' !== 'production' && warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + key); + continue; + } + // record a all lowercase <-> kebab-case mapping for + // possible custom element case error warning + if ('development' !== 'production') { + map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key); + } + def = components[key]; + if (isPlainObject(def)) { + components[key] = Vue.extend(def); + } + } + } + } + + /** + * Ensure all props option syntax are normalized into the + * Object-based format. + * + * @param {Object} options + */ + + function guardProps(options) { + var props = options.props; + var i, val; + if (isArray(props)) { + options.props = {}; + i = props.length; + while (i--) { + val = props[i]; + if (typeof val === 'string') { + options.props[val] = null; + } else if (val.name) { + options.props[val.name] = val; + } + } + } else if (isPlainObject(props)) { + var keys = Object.keys(props); + i = keys.length; + while (i--) { + val = props[keys[i]]; + if (typeof val === 'function') { + props[keys[i]] = { type: val }; + } + } + } + } + + /** + * Guard an Array-format assets option and converted it + * into the key-value Object format. + * + * @param {Object|Array} assets + * @return {Object} + */ + + function guardArrayAssets(assets) { + if (isArray(assets)) { + var res = {}; + var i = assets.length; + var asset; + while (i--) { + asset = assets[i]; + var id = typeof asset === 'function' ? asset.options && asset.options.name || asset.id : asset.name || asset.id; + if (!id) { + 'development' !== 'production' && warn('Array-syntax assets must provide a "name" or "id" field.'); + } else { + res[id] = asset; + } + } + return res; + } + return assets; + } + + /** + * Merge two option objects into a new one. + * Core utility used in both instantiation and inheritance. + * + * @param {Object} parent + * @param {Object} child + * @param {Vue} [vm] - if vm is present, indicates this is + * an instantiation merge. + */ + + function mergeOptions(parent, child, vm) { + guardComponents(child); + guardProps(child); + if ('development' !== 'production') { + if (child.propsData && !vm) { + warn('propsData can only be used as an instantiation option.'); + } + } + var options = {}; + var key; + if (child['extends']) { + parent = typeof child['extends'] === 'function' ? mergeOptions(parent, child['extends'].options, vm) : mergeOptions(parent, child['extends'], vm); + } + if (child.mixins) { + for (var i = 0, l = child.mixins.length; i < l; i++) { + var mixin = child.mixins[i]; + var mixinOptions = mixin.prototype instanceof Vue ? mixin.options : mixin; + parent = mergeOptions(parent, mixinOptions, vm); + } + } + for (key in parent) { + mergeField(key); + } + for (key in child) { + if (!hasOwn(parent, key)) { + mergeField(key); + } + } + function mergeField(key) { + var strat = strats[key] || defaultStrat; + options[key] = strat(parent[key], child[key], vm, key); + } + return options; + } + + /** + * Resolve an asset. + * This function is used because child instances need access + * to assets defined in its ancestor chain. + * + * @param {Object} options + * @param {String} type + * @param {String} id + * @param {Boolean} warnMissing + * @return {Object|Function} + */ + + function resolveAsset(options, type, id, warnMissing) { + /* istanbul ignore if */ + if (typeof id !== 'string') { + return; + } + var assets = options[type]; + var camelizedId; + var res = assets[id] || + // camelCase ID + assets[camelizedId = camelize(id)] || + // Pascal Case ID + assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)]; + if ('development' !== 'production' && warnMissing && !res) { + warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options); + } + return res; + } + + var uid$1 = 0; + + /** + * A dep is an observable that can have multiple + * directives subscribing to it. + * + * @constructor + */ + function Dep() { + this.id = uid$1++; + this.subs = []; + } + + // the current target watcher being evaluated. + // this is globally unique because there could be only one + // watcher being evaluated at any time. + Dep.target = null; + + /** + * Add a directive subscriber. + * + * @param {Directive} sub + */ + + Dep.prototype.addSub = function (sub) { + this.subs.push(sub); + }; + + /** + * Remove a directive subscriber. + * + * @param {Directive} sub + */ + + Dep.prototype.removeSub = function (sub) { + this.subs.$remove(sub); + }; + + /** + * Add self as a dependency to the target watcher. + */ + + Dep.prototype.depend = function () { + Dep.target.addDep(this); + }; + + /** + * Notify all subscribers of a new value. + */ + + Dep.prototype.notify = function () { + // stablize the subscriber list first + var subs = toArray(this.subs); + for (var i = 0, l = subs.length; i < l; i++) { + subs[i].update(); + } + }; + + var arrayProto = Array.prototype; + var arrayMethods = Object.create(arrayProto) + + /** + * Intercept mutating methods and emit events + */ + + ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) { + // cache original method + var original = arrayProto[method]; + def(arrayMethods, method, function mutator() { + // avoid leaking arguments: + // http://jsperf.com/closure-with-arguments + var i = arguments.length; + var args = new Array(i); + while (i--) { + args[i] = arguments[i]; + } + var result = original.apply(this, args); + var ob = this.__ob__; + var inserted; + switch (method) { + case 'push': + inserted = args; + break; + case 'unshift': + inserted = args; + break; + case 'splice': + inserted = args.slice(2); + break; + } + if (inserted) ob.observeArray(inserted); + // notify change + ob.dep.notify(); + return result; + }); + }); + + /** + * Swap the element at the given index with a new value + * and emits corresponding event. + * + * @param {Number} index + * @param {*} val + * @return {*} - replaced element + */ + + def(arrayProto, '$set', function $set(index, val) { + if (index >= this.length) { + this.length = Number(index) + 1; + } + return this.splice(index, 1, val)[0]; + }); + + /** + * Convenience method to remove the element at given index or target element reference. + * + * @param {*} item + */ + + def(arrayProto, '$remove', function $remove(item) { + /* istanbul ignore if */ + if (!this.length) return; + var index = indexOf(this, item); + if (index > -1) { + return this.splice(index, 1); + } + }); + + var arrayKeys = Object.getOwnPropertyNames(arrayMethods); + + /** + * By default, when a reactive property is set, the new value is + * also converted to become reactive. However in certain cases, e.g. + * v-for scope alias and props, we don't want to force conversion + * because the value may be a nested value under a frozen data structure. + * + * So whenever we want to set a reactive property without forcing + * conversion on the new value, we wrap that call inside this function. + */ + + var shouldConvert = true; + + function withoutConversion(fn) { + shouldConvert = false; + fn(); + shouldConvert = true; + } + + /** + * Observer class that are attached to each observed + * object. Once attached, the observer converts target + * object's property keys into getter/setters that + * collect dependencies and dispatches updates. + * + * @param {Array|Object} value + * @constructor + */ + + function Observer(value) { + this.value = value; + this.dep = new Dep(); + def(value, '__ob__', this); + if (isArray(value)) { + var augment = hasProto ? protoAugment : copyAugment; + augment(value, arrayMethods, arrayKeys); + this.observeArray(value); + } else { + this.walk(value); + } + } + + // Instance methods + + /** + * Walk through each property and convert them into + * getter/setters. This method should only be called when + * value type is Object. + * + * @param {Object} obj + */ + + Observer.prototype.walk = function (obj) { + var keys = Object.keys(obj); + for (var i = 0, l = keys.length; i < l; i++) { + this.convert(keys[i], obj[keys[i]]); + } + }; + + /** + * Observe a list of Array items. + * + * @param {Array} items + */ + + Observer.prototype.observeArray = function (items) { + for (var i = 0, l = items.length; i < l; i++) { + observe(items[i]); + } + }; + + /** + * Convert a property into getter/setter so we can emit + * the events when the property is accessed/changed. + * + * @param {String} key + * @param {*} val + */ + + Observer.prototype.convert = function (key, val) { + defineReactive(this.value, key, val); + }; + + /** + * Add an owner vm, so that when $set/$delete mutations + * happen we can notify owner vms to proxy the keys and + * digest the watchers. This is only called when the object + * is observed as an instance's root $data. + * + * @param {Vue} vm + */ + + Observer.prototype.addVm = function (vm) { + (this.vms || (this.vms = [])).push(vm); + }; + + /** + * Remove an owner vm. This is called when the object is + * swapped out as an instance's $data object. + * + * @param {Vue} vm + */ + + Observer.prototype.removeVm = function (vm) { + this.vms.$remove(vm); + }; + + // helpers + + /** + * Augment an target Object or Array by intercepting + * the prototype chain using __proto__ + * + * @param {Object|Array} target + * @param {Object} src + */ + + function protoAugment(target, src) { + /* eslint-disable no-proto */ + target.__proto__ = src; + /* eslint-enable no-proto */ + } + + /** + * Augment an target Object or Array by defining + * hidden properties. + * + * @param {Object|Array} target + * @param {Object} proto + */ + + function copyAugment(target, src, keys) { + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + def(target, key, src[key]); + } + } + + /** + * Attempt to create an observer instance for a value, + * returns the new observer if successfully observed, + * or the existing observer if the value already has one. + * + * @param {*} value + * @param {Vue} [vm] + * @return {Observer|undefined} + * @static + */ + + function observe(value, vm) { + if (!value || typeof value !== 'object') { + return; + } + var ob; + if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { + ob = value.__ob__; + } else if (shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) { + ob = new Observer(value); + } + if (ob && vm) { + ob.addVm(vm); + } + return ob; + } + + /** + * Define a reactive property on an Object. + * + * @param {Object} obj + * @param {String} key + * @param {*} val + */ + + function defineReactive(obj, key, val) { + var dep = new Dep(); + + var property = Object.getOwnPropertyDescriptor(obj, key); + if (property && property.configurable === false) { + return; + } + + // cater for pre-defined getter/setters + var getter = property && property.get; + var setter = property && property.set; + + var childOb = observe(val); + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + var value = getter ? getter.call(obj) : val; + if (Dep.target) { + dep.depend(); + if (childOb) { + childOb.dep.depend(); + } + if (isArray(value)) { + for (var e, i = 0, l = value.length; i < l; i++) { + e = value[i]; + e && e.__ob__ && e.__ob__.dep.depend(); + } + } + } + return value; + }, + set: function reactiveSetter(newVal) { + var value = getter ? getter.call(obj) : val; + if (newVal === value) { + return; + } + if (setter) { + setter.call(obj, newVal); + } else { + val = newVal; + } + childOb = observe(newVal); + dep.notify(); + } + }); + } + + + + var util = Object.freeze({ + defineReactive: defineReactive, + set: set, + del: del, + hasOwn: hasOwn, + isLiteral: isLiteral, + isReserved: isReserved, + _toString: _toString, + toNumber: toNumber, + toBoolean: toBoolean, + stripQuotes: stripQuotes, + camelize: camelize, + hyphenate: hyphenate, + classify: classify, + bind: bind, + toArray: toArray, + extend: extend, + isObject: isObject, + isPlainObject: isPlainObject, + def: def, + debounce: _debounce, + indexOf: indexOf, + cancellable: cancellable, + looseEqual: looseEqual, + isArray: isArray, + hasProto: hasProto, + inBrowser: inBrowser, + devtools: devtools, + isIE: isIE, + isIE9: isIE9, + isAndroid: isAndroid, + isIos: isIos, + iosVersionMatch: iosVersionMatch, + iosVersion: iosVersion, + hasMutationObserverBug: hasMutationObserverBug, + get transitionProp () { return transitionProp; }, + get transitionEndEvent () { return transitionEndEvent; }, + get animationProp () { return animationProp; }, + get animationEndEvent () { return animationEndEvent; }, + nextTick: nextTick, + get _Set () { return _Set; }, + query: query, + inDoc: inDoc, + getAttr: getAttr, + getBindAttr: getBindAttr, + hasBindAttr: hasBindAttr, + before: before, + after: after, + remove: remove, + prepend: prepend, + replace: replace, + on: on, + off: off, + setClass: setClass, + addClass: addClass, + removeClass: removeClass, + extractContent: extractContent, + trimNode: trimNode, + isTemplate: isTemplate, + createAnchor: createAnchor, + findRef: findRef, + mapNodeRange: mapNodeRange, + removeNodeRange: removeNodeRange, + isFragment: isFragment, + getOuterHTML: getOuterHTML, + mergeOptions: mergeOptions, + resolveAsset: resolveAsset, + checkComponentAttr: checkComponentAttr, + commonTagRE: commonTagRE, + reservedTagRE: reservedTagRE, + get warn () { return warn; } + }); + + var uid = 0; + + function initMixin (Vue) { + /** + * The main init sequence. This is called for every + * instance, including ones that are created from extended + * constructors. + * + * @param {Object} options - this options object should be + * the result of merging class + * options and the options passed + * in to the constructor. + */ + + Vue.prototype._init = function (options) { + options = options || {}; + + this.$el = null; + this.$parent = options.parent; + this.$root = this.$parent ? this.$parent.$root : this; + this.$children = []; + this.$refs = {}; // child vm references + this.$els = {}; // element references + this._watchers = []; // all watchers as an array + this._directives = []; // all directives + + // a uid + this._uid = uid++; + + // a flag to avoid this being observed + this._isVue = true; + + // events bookkeeping + this._events = {}; // registered callbacks + this._eventsCount = {}; // for $broadcast optimization + + // fragment instance properties + this._isFragment = false; + this._fragment = // @type {DocumentFragment} + this._fragmentStart = // @type {Text|Comment} + this._fragmentEnd = null; // @type {Text|Comment} + + // lifecycle state + this._isCompiled = this._isDestroyed = this._isReady = this._isAttached = this._isBeingDestroyed = this._vForRemoving = false; + this._unlinkFn = null; + + // context: + // if this is a transcluded component, context + // will be the common parent vm of this instance + // and its host. + this._context = options._context || this.$parent; + + // scope: + // if this is inside an inline v-for, the scope + // will be the intermediate scope created for this + // repeat fragment. this is used for linking props + // and container directives. + this._scope = options._scope; + + // fragment: + // if this instance is compiled inside a Fragment, it + // needs to reigster itself as a child of that fragment + // for attach/detach to work properly. + this._frag = options._frag; + if (this._frag) { + this._frag.children.push(this); + } + + // push self into parent / transclusion host + if (this.$parent) { + this.$parent.$children.push(this); + } + + // merge options. + options = this.$options = mergeOptions(this.constructor.options, options, this); + + // set ref + this._updateRef(); + + // initialize data as empty object. + // it will be filled up in _initData(). + this._data = {}; + + // call init hook + this._callHook('init'); + + // initialize data observation and scope inheritance. + this._initState(); + + // setup event system and option events. + this._initEvents(); + + // call created hook + this._callHook('created'); + + // if `el` option is passed, start compilation. + if (options.el) { + this.$mount(options.el); + } + }; + } + + var pathCache = new Cache(1000); + + // actions + var APPEND = 0; + var PUSH = 1; + var INC_SUB_PATH_DEPTH = 2; + var PUSH_SUB_PATH = 3; + + // states + var BEFORE_PATH = 0; + var IN_PATH = 1; + var BEFORE_IDENT = 2; + var IN_IDENT = 3; + var IN_SUB_PATH = 4; + var IN_SINGLE_QUOTE = 5; + var IN_DOUBLE_QUOTE = 6; + var AFTER_PATH = 7; + var ERROR = 8; + + var pathStateMachine = []; + + pathStateMachine[BEFORE_PATH] = { + 'ws': [BEFORE_PATH], + 'ident': [IN_IDENT, APPEND], + '[': [IN_SUB_PATH], + 'eof': [AFTER_PATH] + }; + + pathStateMachine[IN_PATH] = { + 'ws': [IN_PATH], + '.': [BEFORE_IDENT], + '[': [IN_SUB_PATH], + 'eof': [AFTER_PATH] + }; + + pathStateMachine[BEFORE_IDENT] = { + 'ws': [BEFORE_IDENT], + 'ident': [IN_IDENT, APPEND] + }; + + pathStateMachine[IN_IDENT] = { + 'ident': [IN_IDENT, APPEND], + '0': [IN_IDENT, APPEND], + 'number': [IN_IDENT, APPEND], + 'ws': [IN_PATH, PUSH], + '.': [BEFORE_IDENT, PUSH], + '[': [IN_SUB_PATH, PUSH], + 'eof': [AFTER_PATH, PUSH] + }; + + pathStateMachine[IN_SUB_PATH] = { + "'": [IN_SINGLE_QUOTE, APPEND], + '"': [IN_DOUBLE_QUOTE, APPEND], + '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH], + ']': [IN_PATH, PUSH_SUB_PATH], + 'eof': ERROR, + 'else': [IN_SUB_PATH, APPEND] + }; + + pathStateMachine[IN_SINGLE_QUOTE] = { + "'": [IN_SUB_PATH, APPEND], + 'eof': ERROR, + 'else': [IN_SINGLE_QUOTE, APPEND] + }; + + pathStateMachine[IN_DOUBLE_QUOTE] = { + '"': [IN_SUB_PATH, APPEND], + 'eof': ERROR, + 'else': [IN_DOUBLE_QUOTE, APPEND] + }; + + /** + * Determine the type of a character in a keypath. + * + * @param {Char} ch + * @return {String} type + */ + + function getPathCharType(ch) { + if (ch === undefined) { + return 'eof'; + } + + var code = ch.charCodeAt(0); + + switch (code) { + case 0x5B: // [ + case 0x5D: // ] + case 0x2E: // . + case 0x22: // " + case 0x27: // ' + case 0x30: + // 0 + return ch; + + case 0x5F: // _ + case 0x24: + // $ + return 'ident'; + + case 0x20: // Space + case 0x09: // Tab + case 0x0A: // Newline + case 0x0D: // Return + case 0xA0: // No-break space + case 0xFEFF: // Byte Order Mark + case 0x2028: // Line Separator + case 0x2029: + // Paragraph Separator + return 'ws'; + } + + // a-z, A-Z + if (code >= 0x61 && code <= 0x7A || code >= 0x41 && code <= 0x5A) { + return 'ident'; + } + + // 1-9 + if (code >= 0x31 && code <= 0x39) { + return 'number'; + } + + return 'else'; + } + + /** + * Format a subPath, return its plain form if it is + * a literal string or number. Otherwise prepend the + * dynamic indicator (*). + * + * @param {String} path + * @return {String} + */ + + function formatSubPath(path) { + var trimmed = path.trim(); + // invalid leading 0 + if (path.charAt(0) === '0' && isNaN(path)) { + return false; + } + return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed; + } + + /** + * Parse a string path into an array of segments + * + * @param {String} path + * @return {Array|undefined} + */ + + function parse(path) { + var keys = []; + var index = -1; + var mode = BEFORE_PATH; + var subPathDepth = 0; + var c, newChar, key, type, transition, action, typeMap; + + var actions = []; + + actions[PUSH] = function () { + if (key !== undefined) { + keys.push(key); + key = undefined; + } + }; + + actions[APPEND] = function () { + if (key === undefined) { + key = newChar; + } else { + key += newChar; + } + }; + + actions[INC_SUB_PATH_DEPTH] = function () { + actions[APPEND](); + subPathDepth++; + }; + + actions[PUSH_SUB_PATH] = function () { + if (subPathDepth > 0) { + subPathDepth--; + mode = IN_SUB_PATH; + actions[APPEND](); + } else { + subPathDepth = 0; + key = formatSubPath(key); + if (key === false) { + return false; + } else { + actions[PUSH](); + } + } + }; + + function maybeUnescapeQuote() { + var nextChar = path[index + 1]; + if (mode === IN_SINGLE_QUOTE && nextChar === "'" || mode === IN_DOUBLE_QUOTE && nextChar === '"') { + index++; + newChar = '\\' + nextChar; + actions[APPEND](); + return true; + } + } + + while (mode != null) { + index++; + c = path[index]; + + if (c === '\\' && maybeUnescapeQuote()) { + continue; + } + + type = getPathCharType(c); + typeMap = pathStateMachine[mode]; + transition = typeMap[type] || typeMap['else'] || ERROR; + + if (transition === ERROR) { + return; // parse error + } + + mode = transition[0]; + action = actions[transition[1]]; + if (action) { + newChar = transition[2]; + newChar = newChar === undefined ? c : newChar; + if (action() === false) { + return; + } + } + + if (mode === AFTER_PATH) { + keys.raw = path; + return keys; + } + } + } + + /** + * External parse that check for a cache hit first + * + * @param {String} path + * @return {Array|undefined} + */ + + function parsePath(path) { + var hit = pathCache.get(path); + if (!hit) { + hit = parse(path); + if (hit) { + pathCache.put(path, hit); + } + } + return hit; + } + + /** + * Get from an object from a path string + * + * @param {Object} obj + * @param {String} path + */ + + function getPath(obj, path) { + return parseExpression(path).get(obj); + } + + /** + * Warn against setting non-existent root path on a vm. + */ + + var warnNonExistent; + if ('development' !== 'production') { + warnNonExistent = function (path, vm) { + warn('You are setting a non-existent path "' + path.raw + '" ' + 'on a vm instance. Consider pre-initializing the property ' + 'with the "data" option for more reliable reactivity ' + 'and better performance.', vm); + }; + } + + /** + * Set on an object from a path + * + * @param {Object} obj + * @param {String | Array} path + * @param {*} val + */ + + function setPath(obj, path, val) { + var original = obj; + if (typeof path === 'string') { + path = parse(path); + } + if (!path || !isObject(obj)) { + return false; + } + var last, key; + for (var i = 0, l = path.length; i < l; i++) { + last = obj; + key = path[i]; + if (key.charAt(0) === '*') { + key = parseExpression(key.slice(1)).get.call(original, original); + } + if (i < l - 1) { + obj = obj[key]; + if (!isObject(obj)) { + obj = {}; + if ('development' !== 'production' && last._isVue) { + warnNonExistent(path, last); + } + set(last, key, obj); + } + } else { + if (isArray(obj)) { + obj.$set(key, val); + } else if (key in obj) { + obj[key] = val; + } else { + if ('development' !== 'production' && obj._isVue) { + warnNonExistent(path, obj); + } + set(obj, key, val); + } + } + } + return true; + } + +var path = Object.freeze({ + parsePath: parsePath, + getPath: getPath, + setPath: setPath + }); + + var expressionCache = new Cache(1000); + + var allowedKeywords = 'Math,Date,this,true,false,null,undefined,Infinity,NaN,' + 'isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,' + 'encodeURIComponent,parseInt,parseFloat'; + var allowedKeywordsRE = new RegExp('^(' + allowedKeywords.replace(/,/g, '\\b|') + '\\b)'); + + // keywords that don't make sense inside expressions + var improperKeywords = 'break,case,class,catch,const,continue,debugger,default,' + 'delete,do,else,export,extends,finally,for,function,if,' + 'import,in,instanceof,let,return,super,switch,throw,try,' + 'var,while,with,yield,enum,await,implements,package,' + 'protected,static,interface,private,public'; + var improperKeywordsRE = new RegExp('^(' + improperKeywords.replace(/,/g, '\\b|') + '\\b)'); + + var wsRE = /\s/g; + var newlineRE = /\n/g; + var saveRE = /[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g; + var restoreRE = /"(\d+)"/g; + var pathTestRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/; + var identRE = /[^\w$\.](?:[A-Za-z_$][\w$]*)/g; + var literalValueRE$1 = /^(?:true|false|null|undefined|Infinity|NaN)$/; + + function noop() {} + + /** + * Save / Rewrite / Restore + * + * When rewriting paths found in an expression, it is + * possible for the same letter sequences to be found in + * strings and Object literal property keys. Therefore we + * remove and store these parts in a temporary array, and + * restore them after the path rewrite. + */ + + var saved = []; + + /** + * Save replacer + * + * The save regex can match two possible cases: + * 1. An opening object literal + * 2. A string + * If matched as a plain string, we need to escape its + * newlines, since the string needs to be preserved when + * generating the function body. + * + * @param {String} str + * @param {String} isString - str if matched as a string + * @return {String} - placeholder with index + */ + + function save(str, isString) { + var i = saved.length; + saved[i] = isString ? str.replace(newlineRE, '\\n') : str; + return '"' + i + '"'; + } + + /** + * Path rewrite replacer + * + * @param {String} raw + * @return {String} + */ + + function rewrite(raw) { + var c = raw.charAt(0); + var path = raw.slice(1); + if (allowedKeywordsRE.test(path)) { + return raw; + } else { + path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path; + return c + 'scope.' + path; + } + } + + /** + * Restore replacer + * + * @param {String} str + * @param {String} i - matched save index + * @return {String} + */ + + function restore(str, i) { + return saved[i]; + } + + /** + * Rewrite an expression, prefixing all path accessors with + * `scope.` and generate getter/setter functions. + * + * @param {String} exp + * @return {Function} + */ + + function compileGetter(exp) { + if (improperKeywordsRE.test(exp)) { + 'development' !== 'production' && warn('Avoid using reserved keywords in expression: ' + exp); + } + // reset state + saved.length = 0; + // save strings and object literal keys + var body = exp.replace(saveRE, save).replace(wsRE, ''); + // rewrite all paths + // pad 1 space here because the regex matches 1 extra char + body = (' ' + body).replace(identRE, rewrite).replace(restoreRE, restore); + return makeGetterFn(body); + } + + /** + * Build a getter function. Requires eval. + * + * We isolate the try/catch so it doesn't affect the + * optimization of the parse function when it is not called. + * + * @param {String} body + * @return {Function|undefined} + */ + + function makeGetterFn(body) { + try { + /* eslint-disable no-new-func */ + return new Function('scope', 'return ' + body + ';'); + /* eslint-enable no-new-func */ + } catch (e) { + if ('development' !== 'production') { + /* istanbul ignore if */ + if (e.toString().match(/unsafe-eval|CSP/)) { + warn('It seems you are using the default build of Vue.js in an environment ' + 'with Content Security Policy that prohibits unsafe-eval. ' + 'Use the CSP-compliant build instead: ' + 'http://vuejs.org/guide/installation.html#CSP-compliant-build'); + } else { + warn('Invalid expression. ' + 'Generated function body: ' + body); + } + } + return noop; + } + } + + /** + * Compile a setter function for the expression. + * + * @param {String} exp + * @return {Function|undefined} + */ + + function compileSetter(exp) { + var path = parsePath(exp); + if (path) { + return function (scope, val) { + setPath(scope, path, val); + }; + } else { + 'development' !== 'production' && warn('Invalid setter expression: ' + exp); + } + } + + /** + * Parse an expression into re-written getter/setters. + * + * @param {String} exp + * @param {Boolean} needSet + * @return {Function} + */ + + function parseExpression(exp, needSet) { + exp = exp.trim(); + // try cache + var hit = expressionCache.get(exp); + if (hit) { + if (needSet && !hit.set) { + hit.set = compileSetter(hit.exp); + } + return hit; + } + var res = { exp: exp }; + res.get = isSimplePath(exp) && exp.indexOf('[') < 0 + // optimized super simple getter + ? makeGetterFn('scope.' + exp) + // dynamic getter + : compileGetter(exp); + if (needSet) { + res.set = compileSetter(exp); + } + expressionCache.put(exp, res); + return res; + } + + /** + * Check if an expression is a simple path. + * + * @param {String} exp + * @return {Boolean} + */ + + function isSimplePath(exp) { + return pathTestRE.test(exp) && + // don't treat literal values as paths + !literalValueRE$1.test(exp) && + // Math constants e.g. Math.PI, Math.E etc. + exp.slice(0, 5) !== 'Math.'; + } + +var expression = Object.freeze({ + parseExpression: parseExpression, + isSimplePath: isSimplePath + }); + + // we have two separate queues: one for directive updates + // and one for user watcher registered via $watch(). + // we want to guarantee directive updates to be called + // before user watchers so that when user watchers are + // triggered, the DOM would have already been in updated + // state. + + var queue = []; + var userQueue = []; + var has = {}; + var circular = {}; + var waiting = false; + + /** + * Reset the batcher's state. + */ + + function resetBatcherState() { + queue.length = 0; + userQueue.length = 0; + has = {}; + circular = {}; + waiting = false; + } + + /** + * Flush both queues and run the watchers. + */ + + function flushBatcherQueue() { + var _again = true; + + _function: while (_again) { + _again = false; + + runBatcherQueue(queue); + runBatcherQueue(userQueue); + // user watchers triggered more watchers, + // keep flushing until it depletes + if (queue.length) { + _again = true; + continue _function; + } + // dev tool hook + /* istanbul ignore if */ + if (devtools && config.devtools) { + devtools.emit('flush'); + } + resetBatcherState(); + } + } + + /** + * Run the watchers in a single queue. + * + * @param {Array} queue + */ + + function runBatcherQueue(queue) { + // do not cache length because more watchers might be pushed + // as we run existing watchers + for (var i = 0; i < queue.length; i++) { + var watcher = queue[i]; + var id = watcher.id; + has[id] = null; + watcher.run(); + // in dev build, check and stop circular updates. + if ('development' !== 'production' && has[id] != null) { + circular[id] = (circular[id] || 0) + 1; + if (circular[id] > config._maxUpdateCount) { + warn('You may have an infinite update loop for watcher ' + 'with expression "' + watcher.expression + '"', watcher.vm); + break; + } + } + } + queue.length = 0; + } + + /** + * Push a watcher into the watcher queue. + * Jobs with duplicate IDs will be skipped unless it's + * pushed when the queue is being flushed. + * + * @param {Watcher} watcher + * properties: + * - {Number} id + * - {Function} run + */ + + function pushWatcher(watcher) { + var id = watcher.id; + if (has[id] == null) { + // push watcher into appropriate queue + var q = watcher.user ? userQueue : queue; + has[id] = q.length; + q.push(watcher); + // queue the flush + if (!waiting) { + waiting = true; + nextTick(flushBatcherQueue); + } + } + } + + var uid$2 = 0; + + /** + * A watcher parses an expression, collects dependencies, + * and fires callback when the expression value changes. + * This is used for both the $watch() api and directives. + * + * @param {Vue} vm + * @param {String|Function} expOrFn + * @param {Function} cb + * @param {Object} options + * - {Array} filters + * - {Boolean} twoWay + * - {Boolean} deep + * - {Boolean} user + * - {Boolean} sync + * - {Boolean} lazy + * - {Function} [preProcess] + * - {Function} [postProcess] + * @constructor + */ + function Watcher(vm, expOrFn, cb, options) { + // mix in options + if (options) { + extend(this, options); + } + var isFn = typeof expOrFn === 'function'; + this.vm = vm; + vm._watchers.push(this); + this.expression = expOrFn; + this.cb = cb; + this.id = ++uid$2; // uid for batching + this.active = true; + this.dirty = this.lazy; // for lazy watchers + this.deps = []; + this.newDeps = []; + this.depIds = new _Set(); + this.newDepIds = new _Set(); + this.prevError = null; // for async error stacks + // parse expression for getter/setter + if (isFn) { + this.getter = expOrFn; + this.setter = undefined; + } else { + var res = parseExpression(expOrFn, this.twoWay); + this.getter = res.get; + this.setter = res.set; + } + this.value = this.lazy ? undefined : this.get(); + // state for avoiding false triggers for deep and Array + // watchers during vm._digest() + this.queued = this.shallow = false; + } + + /** + * Evaluate the getter, and re-collect dependencies. + */ + + Watcher.prototype.get = function () { + this.beforeGet(); + var scope = this.scope || this.vm; + var value; + try { + value = this.getter.call(scope, scope); + } catch (e) { + if ('development' !== 'production' && config.warnExpressionErrors) { + warn('Error when evaluating expression ' + '"' + this.expression + '": ' + e.toString(), this.vm); + } + } + // "touch" every property so they are all tracked as + // dependencies for deep watching + if (this.deep) { + traverse(value); + } + if (this.preProcess) { + value = this.preProcess(value); + } + if (this.filters) { + value = scope._applyFilters(value, null, this.filters, false); + } + if (this.postProcess) { + value = this.postProcess(value); + } + this.afterGet(); + return value; + }; + + /** + * Set the corresponding value with the setter. + * + * @param {*} value + */ + + Watcher.prototype.set = function (value) { + var scope = this.scope || this.vm; + if (this.filters) { + value = scope._applyFilters(value, this.value, this.filters, true); + } + try { + this.setter.call(scope, scope, value); + } catch (e) { + if ('development' !== 'production' && config.warnExpressionErrors) { + warn('Error when evaluating setter ' + '"' + this.expression + '": ' + e.toString(), this.vm); + } + } + // two-way sync for v-for alias + var forContext = scope.$forContext; + if (forContext && forContext.alias === this.expression) { + if (forContext.filters) { + 'development' !== 'production' && warn('It seems you are using two-way binding on ' + 'a v-for alias (' + this.expression + '), and the ' + 'v-for has filters. This will not work properly. ' + 'Either remove the filters or use an array of ' + 'objects and bind to object properties instead.', this.vm); + return; + } + forContext._withLock(function () { + if (scope.$key) { + // original is an object + forContext.rawValue[scope.$key] = value; + } else { + forContext.rawValue.$set(scope.$index, value); + } + }); + } + }; + + /** + * Prepare for dependency collection. + */ + + Watcher.prototype.beforeGet = function () { + Dep.target = this; + }; + + /** + * Add a dependency to this directive. + * + * @param {Dep} dep + */ + + Watcher.prototype.addDep = function (dep) { + var id = dep.id; + if (!this.newDepIds.has(id)) { + this.newDepIds.add(id); + this.newDeps.push(dep); + if (!this.depIds.has(id)) { + dep.addSub(this); + } + } + }; + + /** + * Clean up for dependency collection. + */ + + Watcher.prototype.afterGet = function () { + Dep.target = null; + var i = this.deps.length; + while (i--) { + var dep = this.deps[i]; + if (!this.newDepIds.has(dep.id)) { + dep.removeSub(this); + } + } + var tmp = this.depIds; + this.depIds = this.newDepIds; + this.newDepIds = tmp; + this.newDepIds.clear(); + tmp = this.deps; + this.deps = this.newDeps; + this.newDeps = tmp; + this.newDeps.length = 0; + }; + + /** + * Subscriber interface. + * Will be called when a dependency changes. + * + * @param {Boolean} shallow + */ + + Watcher.prototype.update = function (shallow) { + if (this.lazy) { + this.dirty = true; + } else if (this.sync || !config.async) { + this.run(); + } else { + // if queued, only overwrite shallow with non-shallow, + // but not the other way around. + this.shallow = this.queued ? shallow ? this.shallow : false : !!shallow; + this.queued = true; + // record before-push error stack in debug mode + /* istanbul ignore if */ + if ('development' !== 'production' && config.debug) { + this.prevError = new Error('[vue] async stack trace'); + } + pushWatcher(this); + } + }; + + /** + * Batcher job interface. + * Will be called by the batcher. + */ + + Watcher.prototype.run = function () { + if (this.active) { + var value = this.get(); + if (value !== this.value || + // Deep watchers and watchers on Object/Arrays should fire even + // when the value is the same, because the value may + // have mutated; but only do so if this is a + // non-shallow update (caused by a vm digest). + (isObject(value) || this.deep) && !this.shallow) { + // set new value + var oldValue = this.value; + this.value = value; + // in debug + async mode, when a watcher callbacks + // throws, we also throw the saved before-push error + // so the full cross-tick stack trace is available. + var prevError = this.prevError; + /* istanbul ignore if */ + if ('development' !== 'production' && config.debug && prevError) { + this.prevError = null; + try { + this.cb.call(this.vm, value, oldValue); + } catch (e) { + nextTick(function () { + throw prevError; + }, 0); + throw e; + } + } else { + this.cb.call(this.vm, value, oldValue); + } + } + this.queued = this.shallow = false; + } + }; + + /** + * Evaluate the value of the watcher. + * This only gets called for lazy watchers. + */ + + Watcher.prototype.evaluate = function () { + // avoid overwriting another watcher that is being + // collected. + var current = Dep.target; + this.value = this.get(); + this.dirty = false; + Dep.target = current; + }; + + /** + * Depend on all deps collected by this watcher. + */ + + Watcher.prototype.depend = function () { + var i = this.deps.length; + while (i--) { + this.deps[i].depend(); + } + }; + + /** + * Remove self from all dependencies' subcriber list. + */ + + Watcher.prototype.teardown = function () { + if (this.active) { + // remove self from vm's watcher list + // this is a somewhat expensive operation so we skip it + // if the vm is being destroyed or is performing a v-for + // re-render (the watcher list is then filtered by v-for). + if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) { + this.vm._watchers.$remove(this); + } + var i = this.deps.length; + while (i--) { + this.deps[i].removeSub(this); + } + this.active = false; + this.vm = this.cb = this.value = null; + } + }; + + /** + * Recrusively traverse an object to evoke all converted + * getters, so that every nested property inside the object + * is collected as a "deep" dependency. + * + * @param {*} val + */ + + var seenObjects = new _Set(); + function traverse(val, seen) { + var i = undefined, + keys = undefined; + if (!seen) { + seen = seenObjects; + seen.clear(); + } + var isA = isArray(val); + var isO = isObject(val); + if ((isA || isO) && Object.isExtensible(val)) { + if (val.__ob__) { + var depId = val.__ob__.dep.id; + if (seen.has(depId)) { + return; + } else { + seen.add(depId); + } + } + if (isA) { + i = val.length; + while (i--) traverse(val[i], seen); + } else if (isO) { + keys = Object.keys(val); + i = keys.length; + while (i--) traverse(val[keys[i]], seen); + } + } + } + + var text$1 = { + + bind: function bind() { + this.attr = this.el.nodeType === 3 ? 'data' : 'textContent'; + }, + + update: function update(value) { + this.el[this.attr] = _toString(value); + } + }; + + var templateCache = new Cache(1000); + var idSelectorCache = new Cache(1000); + + var map = { + efault: [0, '', ''], + legend: [1, '
', '
'], + tr: [2, '', '
'], + col: [2, '', '
'] + }; + + map.td = map.th = [3, '', '
']; + + map.option = map.optgroup = [1, '']; + + map.thead = map.tbody = map.colgroup = map.caption = map.tfoot = [1, '', '
']; + + map.g = map.defs = map.symbol = map.use = map.image = map.text = map.circle = map.ellipse = map.line = map.path = map.polygon = map.polyline = map.rect = [1, '', '']; + + /** + * Check if a node is a supported template node with a + * DocumentFragment content. + * + * @param {Node} node + * @return {Boolean} + */ + + function isRealTemplate(node) { + return isTemplate(node) && isFragment(node.content); + } + + var tagRE$1 = /<([\w:-]+)/; + var entityRE = /&#?\w+?;/; + var commentRE = / E - } - entry.newer = undefined; // D --x - entry.older = this.tail; // D. --> E - if (this.tail) { - this.tail.newer = entry; // E. <-- D - } - this.tail = entry; - return returnEntry ? entry : entry.value; - }; - - var cache$1 = new Cache(1000); - var filterTokenRE = /[^\s'"]+|'[^']*'|"[^"]*"/g; - var reservedArgRE = /^in$|^-?\d+/; - - /** - * Parser state - */ - - var str; - var dir; - var c; - var prev; - var i; - var l; - var lastFilterIndex; - var inSingle; - var inDouble; - var curly; - var square; - var paren; - /** - * Push a filter to the current directive object - */ - - function pushFilter() { - var exp = str.slice(lastFilterIndex, i).trim(); - var filter; - if (exp) { - filter = {}; - var tokens = exp.match(filterTokenRE); - filter.name = tokens[0]; - if (tokens.length > 1) { - filter.args = tokens.slice(1).map(processFilterArg); - } - } - if (filter) { - (dir.filters = dir.filters || []).push(filter); - } - lastFilterIndex = i + 1; - } - - /** - * Check if an argument is dynamic and strip quotes. - * - * @param {String} arg - * @return {Object} - */ - - function processFilterArg(arg) { - if (reservedArgRE.test(arg)) { - return { - value: toNumber(arg), - dynamic: false - }; - } else { - var stripped = stripQuotes(arg); - var dynamic = stripped === arg; - return { - value: dynamic ? arg : stripped, - dynamic: dynamic - }; - } - } - - /** - * Parse a directive value and extract the expression - * and its filters into a descriptor. - * - * Example: - * - * "a + 1 | uppercase" will yield: - * { - * expression: 'a + 1', - * filters: [ - * { name: 'uppercase', args: null } - * ] - * } - * - * @param {String} s - * @return {Object} - */ - - function parseDirective(s) { - var hit = cache$1.get(s); - if (hit) { - return hit; - } - - // reset parser state - str = s; - inSingle = inDouble = false; - curly = square = paren = 0; - lastFilterIndex = 0; - dir = {}; - - for (i = 0, l = str.length; i < l; i++) { - prev = c; - c = str.charCodeAt(i); - if (inSingle) { - // check single quote - if (c === 0x27 && prev !== 0x5C) inSingle = !inSingle; - } else if (inDouble) { - // check double quote - if (c === 0x22 && prev !== 0x5C) inDouble = !inDouble; - } else if (c === 0x7C && // pipe - str.charCodeAt(i + 1) !== 0x7C && str.charCodeAt(i - 1) !== 0x7C) { - if (dir.expression == null) { - // first filter, end of expression - lastFilterIndex = i + 1; - dir.expression = str.slice(0, i).trim(); - } else { - // already has filter - pushFilter(); - } - } else { - switch (c) { - case 0x22: - inDouble = true;break; // " - case 0x27: - inSingle = true;break; // ' - case 0x28: - paren++;break; // ( - case 0x29: - paren--;break; // ) - case 0x5B: - square++;break; // [ - case 0x5D: - square--;break; // ] - case 0x7B: - curly++;break; // { - case 0x7D: - curly--;break; // } - } - } - } - - if (dir.expression == null) { - dir.expression = str.slice(0, i).trim(); - } else if (lastFilterIndex !== 0) { - pushFilter(); - } - - cache$1.put(s, dir); - return dir; - } - -var directive = Object.freeze({ - parseDirective: parseDirective - }); - - var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g; - var cache = undefined; - var tagRE = undefined; - var htmlRE = undefined; - /** - * Escape a string so it can be used in a RegExp - * constructor. - * - * @param {String} str - */ - - function escapeRegex(str) { - return str.replace(regexEscapeRE, '\\$&'); - } - - function compileRegex() { - var open = escapeRegex(config.delimiters[0]); - var close = escapeRegex(config.delimiters[1]); - var unsafeOpen = escapeRegex(config.unsafeDelimiters[0]); - var unsafeClose = escapeRegex(config.unsafeDelimiters[1]); - tagRE = new RegExp(unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '|' + open + '((?:.|\\n)+?)' + close, 'g'); - htmlRE = new RegExp('^' + unsafeOpen + '((?:.|\\n)+?)' + unsafeClose + '$'); - // reset cache - cache = new Cache(1000); - } - - /** - * Parse a template text string into an array of tokens. - * - * @param {String} text - * @return {Array | null} - * - {String} type - * - {String} value - * - {Boolean} [html] - * - {Boolean} [oneTime] - */ - - function parseText(text) { - if (!cache) { - compileRegex(); - } - var hit = cache.get(text); - if (hit) { - return hit; - } - if (!tagRE.test(text)) { - return null; - } - var tokens = []; - var lastIndex = tagRE.lastIndex = 0; - var match, index, html, value, first, oneTime; - /* eslint-disable no-cond-assign */ - while (match = tagRE.exec(text)) { - /* eslint-enable no-cond-assign */ - index = match.index; - // push text token - if (index > lastIndex) { - tokens.push({ - value: text.slice(lastIndex, index) - }); - } - // tag token - html = htmlRE.test(match[0]); - value = html ? match[1] : match[2]; - first = value.charCodeAt(0); - oneTime = first === 42; // * - value = oneTime ? value.slice(1) : value; - tokens.push({ - tag: true, - value: value.trim(), - html: html, - oneTime: oneTime - }); - lastIndex = index + match[0].length; - } - if (lastIndex < text.length) { - tokens.push({ - value: text.slice(lastIndex) - }); - } - cache.put(text, tokens); - return tokens; - } - - /** - * Format a list of tokens into an expression. - * e.g. tokens parsed from 'a {{b}} c' can be serialized - * into one single expression as '"a " + b + " c"'. - * - * @param {Array} tokens - * @param {Vue} [vm] - * @return {String} - */ - - function tokensToExp(tokens, vm) { - if (tokens.length > 1) { - return tokens.map(function (token) { - return formatToken(token, vm); - }).join('+'); - } else { - return formatToken(tokens[0], vm, true); - } - } - - /** - * Format a single token. - * - * @param {Object} token - * @param {Vue} [vm] - * @param {Boolean} [single] - * @return {String} - */ - - function formatToken(token, vm, single) { - return token.tag ? token.oneTime && vm ? '"' + vm.$eval(token.value) + '"' : inlineFilters(token.value, single) : '"' + token.value + '"'; - } - - /** - * For an attribute with multiple interpolation tags, - * e.g. attr="some-{{thing | filter}}", in order to combine - * the whole thing into a single watchable expression, we - * have to inline those filters. This function does exactly - * that. This is a bit hacky but it avoids heavy changes - * to directive parser and watcher mechanism. - * - * @param {String} exp - * @param {Boolean} single - * @return {String} - */ - - var filterRE = /[^|]\|[^|]/; - function inlineFilters(exp, single) { - if (!filterRE.test(exp)) { - return single ? exp : '(' + exp + ')'; - } else { - var dir = parseDirective(exp); - if (!dir.filters) { - return '(' + exp + ')'; - } else { - return 'this._applyFilters(' + dir.expression + // value - ',null,' + // oldValue (null for read) - JSON.stringify(dir.filters) + // filter descriptors - ',false)'; // write? - } - } - } - -var text = Object.freeze({ - compileRegex: compileRegex, - parseText: parseText, - tokensToExp: tokensToExp - }); - - var delimiters = ['{{', '}}']; - var unsafeDelimiters = ['{{{', '}}}']; - - var config = Object.defineProperties({ - - /** - * Whether to print debug messages. - * Also enables stack trace for warnings. - * - * @type {Boolean} - */ - - debug: false, - - /** - * Whether to suppress warnings. - * - * @type {Boolean} - */ - - silent: false, - - /** - * Whether to use async rendering. - */ - - async: true, - - /** - * Whether to warn against errors caught when evaluating - * expressions. - */ - - warnExpressionErrors: true, - - /** - * Whether to allow devtools inspection. - * Disabled by default in production builds. - */ - - devtools: 'development' !== 'production', - - /** - * Internal flag to indicate the delimiters have been - * changed. - * - * @type {Boolean} - */ - - _delimitersChanged: true, - - /** - * List of asset types that a component can own. - * - * @type {Array} - */ - - _assetTypes: ['component', 'directive', 'elementDirective', 'filter', 'transition', 'partial'], - - /** - * prop binding modes - */ - - _propBindingModes: { - ONE_WAY: 0, - TWO_WAY: 1, - ONE_TIME: 2 - }, - - /** - * Max circular updates allowed in a batcher flush cycle. - */ - - _maxUpdateCount: 100 - - }, { - delimiters: { /** - * Interpolation delimiters. Changing these would trigger - * the text parser to re-compile the regular expressions. - * - * @type {Array} - */ - - get: function get() { - return delimiters; - }, - set: function set(val) { - delimiters = val; - compileRegex(); - }, - configurable: true, - enumerable: true - }, - unsafeDelimiters: { - get: function get() { - return unsafeDelimiters; - }, - set: function set(val) { - unsafeDelimiters = val; - compileRegex(); - }, - configurable: true, - enumerable: true - } - }); - - var warn = undefined; - var formatComponentName = undefined; - - if ('development' !== 'production') { - (function () { - var hasConsole = typeof console !== 'undefined'; - - warn = function (msg, vm) { - if (hasConsole && !config.silent) { - console.error('[Vue warn]: ' + msg + (vm ? formatComponentName(vm) : '')); - } - }; - - formatComponentName = function (vm) { - var name = vm._isVue ? vm.$options.name : vm.name; - return name ? ' (found in component: <' + hyphenate(name) + '>)' : ''; - }; - })(); - } - - /** - * Append with transition. - * - * @param {Element} el - * @param {Element} target - * @param {Vue} vm - * @param {Function} [cb] - */ - - function appendWithTransition(el, target, vm, cb) { - applyTransition(el, 1, function () { - target.appendChild(el); - }, vm, cb); - } - - /** - * InsertBefore with transition. - * - * @param {Element} el - * @param {Element} target - * @param {Vue} vm - * @param {Function} [cb] - */ - - function beforeWithTransition(el, target, vm, cb) { - applyTransition(el, 1, function () { - before(el, target); - }, vm, cb); - } - - /** - * Remove with transition. - * - * @param {Element} el - * @param {Vue} vm - * @param {Function} [cb] - */ - - function removeWithTransition(el, vm, cb) { - applyTransition(el, -1, function () { - remove(el); - }, vm, cb); - } - - /** - * Apply transitions with an operation callback. - * - * @param {Element} el - * @param {Number} direction - * 1: enter - * -1: leave - * @param {Function} op - the actual DOM operation - * @param {Vue} vm - * @param {Function} [cb] - */ - - function applyTransition(el, direction, op, vm, cb) { - var transition = el.__v_trans; - if (!transition || - // skip if there are no js hooks and CSS transition is - // not supported - !transition.hooks && !transitionEndEvent || - // skip transitions for initial compile - !vm._isCompiled || - // if the vm is being manipulated by a parent directive - // during the parent's compilation phase, skip the - // animation. - vm.$parent && !vm.$parent._isCompiled) { - op(); - if (cb) cb(); - return; - } - var action = direction > 0 ? 'enter' : 'leave'; - transition[action](op, cb); - } - -var transition = Object.freeze({ - appendWithTransition: appendWithTransition, - beforeWithTransition: beforeWithTransition, - removeWithTransition: removeWithTransition, - applyTransition: applyTransition - }); - - /** - * Query an element selector if it's not an element already. - * - * @param {String|Element} el - * @return {Element} - */ - - function query(el) { - if (typeof el === 'string') { - var selector = el; - el = document.querySelector(el); - if (!el) { - 'development' !== 'production' && warn('Cannot find element: ' + selector); - } - } - return el; - } - - /** - * Check if a node is in the document. - * Note: document.documentElement.contains should work here - * but always returns false for comment nodes in phantomjs, - * making unit tests difficult. This is fixed by doing the - * contains() check on the node's parentNode instead of - * the node itself. - * - * @param {Node} node - * @return {Boolean} - */ - - function inDoc(node) { - if (!node) return false; - var doc = node.ownerDocument.documentElement; - var parent = node.parentNode; - return doc === node || doc === parent || !!(parent && parent.nodeType === 1 && doc.contains(parent)); - } - - /** - * Get and remove an attribute from a node. - * - * @param {Node} node - * @param {String} _attr - */ - - function getAttr(node, _attr) { - var val = node.getAttribute(_attr); - if (val !== null) { - node.removeAttribute(_attr); - } - return val; - } - - /** - * Get an attribute with colon or v-bind: prefix. - * - * @param {Node} node - * @param {String} name - * @return {String|null} - */ - - function getBindAttr(node, name) { - var val = getAttr(node, ':' + name); - if (val === null) { - val = getAttr(node, 'v-bind:' + name); - } - return val; - } - - /** - * Check the presence of a bind attribute. - * - * @param {Node} node - * @param {String} name - * @return {Boolean} - */ - - function hasBindAttr(node, name) { - return node.hasAttribute(name) || node.hasAttribute(':' + name) || node.hasAttribute('v-bind:' + name); - } - - /** - * Insert el before target - * - * @param {Element} el - * @param {Element} target - */ - - function before(el, target) { - target.parentNode.insertBefore(el, target); - } - - /** - * Insert el after target - * - * @param {Element} el - * @param {Element} target - */ - - function after(el, target) { - if (target.nextSibling) { - before(el, target.nextSibling); - } else { - target.parentNode.appendChild(el); - } - } - - /** - * Remove el from DOM - * - * @param {Element} el - */ - - function remove(el) { - el.parentNode.removeChild(el); - } - - /** - * Prepend el to target - * - * @param {Element} el - * @param {Element} target - */ - - function prepend(el, target) { - if (target.firstChild) { - before(el, target.firstChild); - } else { - target.appendChild(el); - } - } - - /** - * Replace target with el - * - * @param {Element} target - * @param {Element} el - */ - - function replace(target, el) { - var parent = target.parentNode; - if (parent) { - parent.replaceChild(el, target); - } - } - - /** - * Add event listener shorthand. - * - * @param {Element} el - * @param {String} event - * @param {Function} cb - * @param {Boolean} [useCapture] - */ - - function on(el, event, cb, useCapture) { - el.addEventListener(event, cb, useCapture); - } - - /** - * Remove event listener shorthand. - * - * @param {Element} el - * @param {String} event - * @param {Function} cb - */ - - function off(el, event, cb) { - el.removeEventListener(event, cb); - } - - /** - * For IE9 compat: when both class and :class are present - * getAttribute('class') returns wrong value... - * - * @param {Element} el - * @return {String} - */ - - function getClass(el) { - var classname = el.className; - if (typeof classname === 'object') { - classname = classname.baseVal || ''; - } - return classname; - } - - /** - * In IE9, setAttribute('class') will result in empty class - * if the element also has the :class attribute; However in - * PhantomJS, setting `className` does not work on SVG elements... - * So we have to do a conditional check here. - * - * @param {Element} el - * @param {String} cls - */ - - function setClass(el, cls) { - /* istanbul ignore if */ - if (isIE9 && !/svg$/.test(el.namespaceURI)) { - el.className = cls; - } else { - el.setAttribute('class', cls); - } - } - - /** - * Add class with compatibility for IE & SVG - * - * @param {Element} el - * @param {String} cls - */ - - function addClass(el, cls) { - if (el.classList) { - el.classList.add(cls); - } else { - var cur = ' ' + getClass(el) + ' '; - if (cur.indexOf(' ' + cls + ' ') < 0) { - setClass(el, (cur + cls).trim()); - } - } - } - - /** - * Remove class with compatibility for IE & SVG - * - * @param {Element} el - * @param {String} cls - */ - - function removeClass(el, cls) { - if (el.classList) { - el.classList.remove(cls); - } else { - var cur = ' ' + getClass(el) + ' '; - var tar = ' ' + cls + ' '; - while (cur.indexOf(tar) >= 0) { - cur = cur.replace(tar, ' '); - } - setClass(el, cur.trim()); - } - if (!el.className) { - el.removeAttribute('class'); - } - } - - /** - * Extract raw content inside an element into a temporary - * container div - * - * @param {Element} el - * @param {Boolean} asFragment - * @return {Element|DocumentFragment} - */ - - function extractContent(el, asFragment) { - var child; - var rawContent; - /* istanbul ignore if */ - if (isTemplate(el) && isFragment(el.content)) { - el = el.content; - } - if (el.hasChildNodes()) { - trimNode(el); - rawContent = asFragment ? document.createDocumentFragment() : document.createElement('div'); - /* eslint-disable no-cond-assign */ - while (child = el.firstChild) { - /* eslint-enable no-cond-assign */ - rawContent.appendChild(child); - } - } - return rawContent; - } - - /** - * Trim possible empty head/tail text and comment - * nodes inside a parent. - * - * @param {Node} node - */ - - function trimNode(node) { - var child; - /* eslint-disable no-sequences */ - while ((child = node.firstChild, isTrimmable(child))) { - node.removeChild(child); - } - while ((child = node.lastChild, isTrimmable(child))) { - node.removeChild(child); - } - /* eslint-enable no-sequences */ - } - - function isTrimmable(node) { - return node && (node.nodeType === 3 && !node.data.trim() || node.nodeType === 8); - } - - /** - * Check if an element is a template tag. - * Note if the template appears inside an SVG its tagName - * will be in lowercase. - * - * @param {Element} el - */ - - function isTemplate(el) { - return el.tagName && el.tagName.toLowerCase() === 'template'; - } - - /** - * Create an "anchor" for performing dom insertion/removals. - * This is used in a number of scenarios: - * - fragment instance - * - v-html - * - v-if - * - v-for - * - component - * - * @param {String} content - * @param {Boolean} persist - IE trashes empty textNodes on - * cloneNode(true), so in certain - * cases the anchor needs to be - * non-empty to be persisted in - * templates. - * @return {Comment|Text} - */ - - function createAnchor(content, persist) { - var anchor = config.debug ? document.createComment(content) : document.createTextNode(persist ? ' ' : ''); - anchor.__v_anchor = true; - return anchor; - } - - /** - * Find a component ref attribute that starts with $. - * - * @param {Element} node - * @return {String|undefined} - */ - - var refRE = /^v-ref:/; - - function findRef(node) { - if (node.hasAttributes()) { - var attrs = node.attributes; - for (var i = 0, l = attrs.length; i < l; i++) { - var name = attrs[i].name; - if (refRE.test(name)) { - return camelize(name.replace(refRE, '')); - } - } - } - } - - /** - * Map a function to a range of nodes . - * - * @param {Node} node - * @param {Node} end - * @param {Function} op - */ - - function mapNodeRange(node, end, op) { - var next; - while (node !== end) { - next = node.nextSibling; - op(node); - node = next; - } - op(end); - } - - /** - * Remove a range of nodes with transition, store - * the nodes in a fragment with correct ordering, - * and call callback when done. - * - * @param {Node} start - * @param {Node} end - * @param {Vue} vm - * @param {DocumentFragment} frag - * @param {Function} cb - */ - - function removeNodeRange(start, end, vm, frag, cb) { - var done = false; - var removed = 0; - var nodes = []; - mapNodeRange(start, end, function (node) { - if (node === end) done = true; - nodes.push(node); - removeWithTransition(node, vm, onRemoved); - }); - function onRemoved() { - removed++; - if (done && removed >= nodes.length) { - for (var i = 0; i < nodes.length; i++) { - frag.appendChild(nodes[i]); - } - cb && cb(); - } - } - } - - /** - * Check if a node is a DocumentFragment. - * - * @param {Node} node - * @return {Boolean} - */ - - function isFragment(node) { - return node && node.nodeType === 11; - } - - /** - * Get outerHTML of elements, taking care - * of SVG elements in IE as well. - * - * @param {Element} el - * @return {String} - */ - - function getOuterHTML(el) { - if (el.outerHTML) { - return el.outerHTML; - } else { - var container = document.createElement('div'); - container.appendChild(el.cloneNode(true)); - return container.innerHTML; - } - } - - var commonTagRE = /^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i; - var reservedTagRE = /^(slot|partial|component)$/i; - - var isUnknownElement = undefined; - if ('development' !== 'production') { - isUnknownElement = function (el, tag) { - if (tag.indexOf('-') > -1) { - // http://stackoverflow.com/a/28210364/1070244 - return el.constructor === window.HTMLUnknownElement || el.constructor === window.HTMLElement; - } else { - return (/HTMLUnknownElement/.test(el.toString()) && - // Chrome returns unknown for several HTML5 elements. - // https://code.google.com/p/chromium/issues/detail?id=540526 - // Firefox returns unknown for some "Interactive elements." - !/^(data|time|rtc|rb|details|dialog|summary)$/.test(tag) - ); - } - }; - } - - /** - * Check if an element is a component, if yes return its - * component id. - * - * @param {Element} el - * @param {Object} options - * @return {Object|undefined} - */ - - function checkComponentAttr(el, options) { - var tag = el.tagName.toLowerCase(); - var hasAttrs = el.hasAttributes(); - if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) { - if (resolveAsset(options, 'components', tag)) { - return { id: tag }; - } else { - var is = hasAttrs && getIsBinding(el, options); - if (is) { - return is; - } else if ('development' !== 'production') { - var expectedTag = options._componentNameMap && options._componentNameMap[tag]; - if (expectedTag) { - warn('Unknown custom element: <' + tag + '> - ' + 'did you mean <' + expectedTag + '>? ' + 'HTML is case-insensitive, remember to use kebab-case in templates.'); - } else if (isUnknownElement(el, tag)) { - warn('Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.'); - } - } - } - } else if (hasAttrs) { - return getIsBinding(el, options); - } - } - - /** - * Get "is" binding from an element. - * - * @param {Element} el - * @param {Object} options - * @return {Object|undefined} - */ - - function getIsBinding(el, options) { - // dynamic syntax - var exp = el.getAttribute('is'); - if (exp != null) { - if (resolveAsset(options, 'components', exp)) { - el.removeAttribute('is'); - return { id: exp }; - } - } else { - exp = getBindAttr(el, 'is'); - if (exp != null) { - return { id: exp, dynamic: true }; - } - } - } - - /** - * Option overwriting strategies are functions that handle - * how to merge a parent option value and a child option - * value into the final value. - * - * All strategy functions follow the same signature: - * - * @param {*} parentVal - * @param {*} childVal - * @param {Vue} [vm] - */ - - var strats = config.optionMergeStrategies = Object.create(null); - - /** - * Helper that recursively merges two data objects together. - */ - - function mergeData(to, from) { - var key, toVal, fromVal; - for (key in from) { - toVal = to[key]; - fromVal = from[key]; - if (!hasOwn(to, key)) { - set(to, key, fromVal); - } else if (isObject(toVal) && isObject(fromVal)) { - mergeData(toVal, fromVal); - } - } - return to; - } - - /** - * Data - */ - - strats.data = function (parentVal, childVal, vm) { - if (!vm) { - // in a Vue.extend merge, both should be functions - if (!childVal) { - return parentVal; - } - if (typeof childVal !== 'function') { - 'development' !== 'production' && warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm); - return parentVal; - } - if (!parentVal) { - return childVal; - } - // when parentVal & childVal are both present, - // we need to return a function that returns the - // merged result of both functions... no need to - // check if parentVal is a function here because - // it has to be a function to pass previous merges. - return function mergedDataFn() { - return mergeData(childVal.call(this), parentVal.call(this)); - }; - } else if (parentVal || childVal) { - return function mergedInstanceDataFn() { - // instance merge - var instanceData = typeof childVal === 'function' ? childVal.call(vm) : childVal; - var defaultData = typeof parentVal === 'function' ? parentVal.call(vm) : undefined; - if (instanceData) { - return mergeData(instanceData, defaultData); - } else { - return defaultData; - } - }; - } - }; - - /** - * El - */ - - strats.el = function (parentVal, childVal, vm) { - if (!vm && childVal && typeof childVal !== 'function') { - 'development' !== 'production' && warn('The "el" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.', vm); - return; - } - var ret = childVal || parentVal; - // invoke the element factory if this is instance merge - return vm && typeof ret === 'function' ? ret.call(vm) : ret; - }; - - /** - * Hooks and param attributes are merged as arrays. - */ - - strats.init = strats.created = strats.ready = strats.attached = strats.detached = strats.beforeCompile = strats.compiled = strats.beforeDestroy = strats.destroyed = strats.activate = function (parentVal, childVal) { - return childVal ? parentVal ? parentVal.concat(childVal) : isArray(childVal) ? childVal : [childVal] : parentVal; - }; - - /** - * Assets - * - * When a vm is present (instance creation), we need to do - * a three-way merge between constructor options, instance - * options and parent options. - */ - - function mergeAssets(parentVal, childVal) { - var res = Object.create(parentVal || null); - return childVal ? extend(res, guardArrayAssets(childVal)) : res; - } - - config._assetTypes.forEach(function (type) { - strats[type + 's'] = mergeAssets; - }); - - /** - * Events & Watchers. - * - * Events & watchers hashes should not overwrite one - * another, so we merge them as arrays. - */ - - strats.watch = strats.events = function (parentVal, childVal) { - if (!childVal) return parentVal; - if (!parentVal) return childVal; - var ret = {}; - extend(ret, parentVal); - for (var key in childVal) { - var parent = ret[key]; - var child = childVal[key]; - if (parent && !isArray(parent)) { - parent = [parent]; - } - ret[key] = parent ? parent.concat(child) : [child]; - } - return ret; - }; - - /** - * Other object hashes. - */ - - strats.props = strats.methods = strats.computed = function (parentVal, childVal) { - if (!childVal) return parentVal; - if (!parentVal) return childVal; - var ret = Object.create(null); - extend(ret, parentVal); - extend(ret, childVal); - return ret; - }; - - /** - * Default strategy. - */ - - var defaultStrat = function defaultStrat(parentVal, childVal) { - return childVal === undefined ? parentVal : childVal; - }; - - /** - * Make sure component options get converted to actual - * constructors. - * - * @param {Object} options - */ - - function guardComponents(options) { - if (options.components) { - var components = options.components = guardArrayAssets(options.components); - var ids = Object.keys(components); - var def; - if ('development' !== 'production') { - var map = options._componentNameMap = {}; - } - for (var i = 0, l = ids.length; i < l; i++) { - var key = ids[i]; - if (commonTagRE.test(key) || reservedTagRE.test(key)) { - 'development' !== 'production' && warn('Do not use built-in or reserved HTML elements as component ' + 'id: ' + key); - continue; - } - // record a all lowercase <-> kebab-case mapping for - // possible custom element case error warning - if ('development' !== 'production') { - map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key); - } - def = components[key]; - if (isPlainObject(def)) { - components[key] = Vue.extend(def); - } - } - } - } - - /** - * Ensure all props option syntax are normalized into the - * Object-based format. - * - * @param {Object} options - */ - - function guardProps(options) { - var props = options.props; - var i, val; - if (isArray(props)) { - options.props = {}; - i = props.length; - while (i--) { - val = props[i]; - if (typeof val === 'string') { - options.props[val] = null; - } else if (val.name) { - options.props[val.name] = val; - } - } - } else if (isPlainObject(props)) { - var keys = Object.keys(props); - i = keys.length; - while (i--) { - val = props[keys[i]]; - if (typeof val === 'function') { - props[keys[i]] = { type: val }; - } - } - } - } - - /** - * Guard an Array-format assets option and converted it - * into the key-value Object format. - * - * @param {Object|Array} assets - * @return {Object} - */ - - function guardArrayAssets(assets) { - if (isArray(assets)) { - var res = {}; - var i = assets.length; - var asset; - while (i--) { - asset = assets[i]; - var id = typeof asset === 'function' ? asset.options && asset.options.name || asset.id : asset.name || asset.id; - if (!id) { - 'development' !== 'production' && warn('Array-syntax assets must provide a "name" or "id" field.'); - } else { - res[id] = asset; - } - } - return res; - } - return assets; - } - - /** - * Merge two option objects into a new one. - * Core utility used in both instantiation and inheritance. - * - * @param {Object} parent - * @param {Object} child - * @param {Vue} [vm] - if vm is present, indicates this is - * an instantiation merge. - */ - - function mergeOptions(parent, child, vm) { - guardComponents(child); - guardProps(child); - if ('development' !== 'production') { - if (child.propsData && !vm) { - warn('propsData can only be used as an instantiation option.'); - } - } - var options = {}; - var key; - if (child['extends']) { - parent = typeof child['extends'] === 'function' ? mergeOptions(parent, child['extends'].options, vm) : mergeOptions(parent, child['extends'], vm); - } - if (child.mixins) { - for (var i = 0, l = child.mixins.length; i < l; i++) { - var mixin = child.mixins[i]; - var mixinOptions = mixin.prototype instanceof Vue ? mixin.options : mixin; - parent = mergeOptions(parent, mixinOptions, vm); - } - } - for (key in parent) { - mergeField(key); - } - for (key in child) { - if (!hasOwn(parent, key)) { - mergeField(key); - } - } - function mergeField(key) { - var strat = strats[key] || defaultStrat; - options[key] = strat(parent[key], child[key], vm, key); - } - return options; - } - - /** - * Resolve an asset. - * This function is used because child instances need access - * to assets defined in its ancestor chain. - * - * @param {Object} options - * @param {String} type - * @param {String} id - * @param {Boolean} warnMissing - * @return {Object|Function} - */ - - function resolveAsset(options, type, id, warnMissing) { - /* istanbul ignore if */ - if (typeof id !== 'string') { - return; - } - var assets = options[type]; - var camelizedId; - var res = assets[id] || - // camelCase ID - assets[camelizedId = camelize(id)] || - // Pascal Case ID - assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)]; - if ('development' !== 'production' && warnMissing && !res) { - warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options); - } - return res; - } - - var uid$1 = 0; - - /** - * A dep is an observable that can have multiple - * directives subscribing to it. - * - * @constructor - */ - function Dep() { - this.id = uid$1++; - this.subs = []; - } - - // the current target watcher being evaluated. - // this is globally unique because there could be only one - // watcher being evaluated at any time. - Dep.target = null; - - /** - * Add a directive subscriber. - * - * @param {Directive} sub - */ - - Dep.prototype.addSub = function (sub) { - this.subs.push(sub); - }; - - /** - * Remove a directive subscriber. - * - * @param {Directive} sub - */ - - Dep.prototype.removeSub = function (sub) { - this.subs.$remove(sub); - }; - - /** - * Add self as a dependency to the target watcher. - */ - - Dep.prototype.depend = function () { - Dep.target.addDep(this); - }; - - /** - * Notify all subscribers of a new value. - */ - - Dep.prototype.notify = function () { - // stablize the subscriber list first - var subs = toArray(this.subs); - for (var i = 0, l = subs.length; i < l; i++) { - subs[i].update(); - } - }; - - var arrayProto = Array.prototype; - var arrayMethods = Object.create(arrayProto) - - /** - * Intercept mutating methods and emit events - */ - - ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) { - // cache original method - var original = arrayProto[method]; - def(arrayMethods, method, function mutator() { - // avoid leaking arguments: - // http://jsperf.com/closure-with-arguments - var i = arguments.length; - var args = new Array(i); - while (i--) { - args[i] = arguments[i]; - } - var result = original.apply(this, args); - var ob = this.__ob__; - var inserted; - switch (method) { - case 'push': - inserted = args; - break; - case 'unshift': - inserted = args; - break; - case 'splice': - inserted = args.slice(2); - break; - } - if (inserted) ob.observeArray(inserted); - // notify change - ob.dep.notify(); - return result; - }); - }); - - /** - * Swap the element at the given index with a new value - * and emits corresponding event. - * - * @param {Number} index - * @param {*} val - * @return {*} - replaced element - */ - - def(arrayProto, '$set', function $set(index, val) { - if (index >= this.length) { - this.length = Number(index) + 1; - } - return this.splice(index, 1, val)[0]; - }); - - /** - * Convenience method to remove the element at given index or target element reference. - * - * @param {*} item - */ - - def(arrayProto, '$remove', function $remove(item) { - /* istanbul ignore if */ - if (!this.length) return; - var index = indexOf(this, item); - if (index > -1) { - return this.splice(index, 1); - } - }); - - var arrayKeys = Object.getOwnPropertyNames(arrayMethods); - - /** - * By default, when a reactive property is set, the new value is - * also converted to become reactive. However in certain cases, e.g. - * v-for scope alias and props, we don't want to force conversion - * because the value may be a nested value under a frozen data structure. - * - * So whenever we want to set a reactive property without forcing - * conversion on the new value, we wrap that call inside this function. - */ - - var shouldConvert = true; - - function withoutConversion(fn) { - shouldConvert = false; - fn(); - shouldConvert = true; - } - - /** - * Observer class that are attached to each observed - * object. Once attached, the observer converts target - * object's property keys into getter/setters that - * collect dependencies and dispatches updates. - * - * @param {Array|Object} value - * @constructor - */ - - function Observer(value) { - this.value = value; - this.dep = new Dep(); - def(value, '__ob__', this); - if (isArray(value)) { - var augment = hasProto ? protoAugment : copyAugment; - augment(value, arrayMethods, arrayKeys); - this.observeArray(value); - } else { - this.walk(value); - } - } - - // Instance methods - - /** - * Walk through each property and convert them into - * getter/setters. This method should only be called when - * value type is Object. - * - * @param {Object} obj - */ - - Observer.prototype.walk = function (obj) { - var keys = Object.keys(obj); - for (var i = 0, l = keys.length; i < l; i++) { - this.convert(keys[i], obj[keys[i]]); - } - }; - - /** - * Observe a list of Array items. - * - * @param {Array} items - */ - - Observer.prototype.observeArray = function (items) { - for (var i = 0, l = items.length; i < l; i++) { - observe(items[i]); - } - }; - - /** - * Convert a property into getter/setter so we can emit - * the events when the property is accessed/changed. - * - * @param {String} key - * @param {*} val - */ - - Observer.prototype.convert = function (key, val) { - defineReactive(this.value, key, val); - }; - - /** - * Add an owner vm, so that when $set/$delete mutations - * happen we can notify owner vms to proxy the keys and - * digest the watchers. This is only called when the object - * is observed as an instance's root $data. - * - * @param {Vue} vm - */ - - Observer.prototype.addVm = function (vm) { - (this.vms || (this.vms = [])).push(vm); - }; - - /** - * Remove an owner vm. This is called when the object is - * swapped out as an instance's $data object. - * - * @param {Vue} vm - */ - - Observer.prototype.removeVm = function (vm) { - this.vms.$remove(vm); - }; - - // helpers - - /** - * Augment an target Object or Array by intercepting - * the prototype chain using __proto__ - * - * @param {Object|Array} target - * @param {Object} src - */ - - function protoAugment(target, src) { - /* eslint-disable no-proto */ - target.__proto__ = src; - /* eslint-enable no-proto */ - } - - /** - * Augment an target Object or Array by defining - * hidden properties. - * - * @param {Object|Array} target - * @param {Object} proto - */ - - function copyAugment(target, src, keys) { - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - def(target, key, src[key]); - } - } - - /** - * Attempt to create an observer instance for a value, - * returns the new observer if successfully observed, - * or the existing observer if the value already has one. - * - * @param {*} value - * @param {Vue} [vm] - * @return {Observer|undefined} - * @static - */ - - function observe(value, vm) { - if (!value || typeof value !== 'object') { - return; - } - var ob; - if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { - ob = value.__ob__; - } else if (shouldConvert && (isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) { - ob = new Observer(value); - } - if (ob && vm) { - ob.addVm(vm); - } - return ob; - } - - /** - * Define a reactive property on an Object. - * - * @param {Object} obj - * @param {String} key - * @param {*} val - */ - - function defineReactive(obj, key, val) { - var dep = new Dep(); - - var property = Object.getOwnPropertyDescriptor(obj, key); - if (property && property.configurable === false) { - return; - } - - // cater for pre-defined getter/setters - var getter = property && property.get; - var setter = property && property.set; - - var childOb = observe(val); - Object.defineProperty(obj, key, { - enumerable: true, - configurable: true, - get: function reactiveGetter() { - var value = getter ? getter.call(obj) : val; - if (Dep.target) { - dep.depend(); - if (childOb) { - childOb.dep.depend(); - } - if (isArray(value)) { - for (var e, i = 0, l = value.length; i < l; i++) { - e = value[i]; - e && e.__ob__ && e.__ob__.dep.depend(); - } - } - } - return value; - }, - set: function reactiveSetter(newVal) { - var value = getter ? getter.call(obj) : val; - if (newVal === value) { - return; - } - if (setter) { - setter.call(obj, newVal); - } else { - val = newVal; - } - childOb = observe(newVal); - dep.notify(); - } - }); - } - - - - var util = Object.freeze({ - defineReactive: defineReactive, - set: set, - del: del, - hasOwn: hasOwn, - isLiteral: isLiteral, - isReserved: isReserved, - _toString: _toString, - toNumber: toNumber, - toBoolean: toBoolean, - stripQuotes: stripQuotes, - camelize: camelize, - hyphenate: hyphenate, - classify: classify, - bind: bind, - toArray: toArray, - extend: extend, - isObject: isObject, - isPlainObject: isPlainObject, - def: def, - debounce: _debounce, - indexOf: indexOf, - cancellable: cancellable, - looseEqual: looseEqual, - isArray: isArray, - hasProto: hasProto, - inBrowser: inBrowser, - devtools: devtools, - isIE: isIE, - isIE9: isIE9, - isAndroid: isAndroid, - isIos: isIos, - iosVersionMatch: iosVersionMatch, - iosVersion: iosVersion, - hasMutationObserverBug: hasMutationObserverBug, - get transitionProp () { return transitionProp; }, - get transitionEndEvent () { return transitionEndEvent; }, - get animationProp () { return animationProp; }, - get animationEndEvent () { return animationEndEvent; }, - nextTick: nextTick, - get _Set () { return _Set; }, - query: query, - inDoc: inDoc, - getAttr: getAttr, - getBindAttr: getBindAttr, - hasBindAttr: hasBindAttr, - before: before, - after: after, - remove: remove, - prepend: prepend, - replace: replace, - on: on, - off: off, - setClass: setClass, - addClass: addClass, - removeClass: removeClass, - extractContent: extractContent, - trimNode: trimNode, - isTemplate: isTemplate, - createAnchor: createAnchor, - findRef: findRef, - mapNodeRange: mapNodeRange, - removeNodeRange: removeNodeRange, - isFragment: isFragment, - getOuterHTML: getOuterHTML, - mergeOptions: mergeOptions, - resolveAsset: resolveAsset, - checkComponentAttr: checkComponentAttr, - commonTagRE: commonTagRE, - reservedTagRE: reservedTagRE, - get warn () { return warn; } - }); - - var uid = 0; - - function initMixin (Vue) { - /** - * The main init sequence. This is called for every - * instance, including ones that are created from extended - * constructors. - * - * @param {Object} options - this options object should be - * the result of merging class - * options and the options passed - * in to the constructor. - */ - - Vue.prototype._init = function (options) { - options = options || {}; - - this.$el = null; - this.$parent = options.parent; - this.$root = this.$parent ? this.$parent.$root : this; - this.$children = []; - this.$refs = {}; // child vm references - this.$els = {}; // element references - this._watchers = []; // all watchers as an array - this._directives = []; // all directives - - // a uid - this._uid = uid++; - - // a flag to avoid this being observed - this._isVue = true; - - // events bookkeeping - this._events = {}; // registered callbacks - this._eventsCount = {}; // for $broadcast optimization - - // fragment instance properties - this._isFragment = false; - this._fragment = // @type {DocumentFragment} - this._fragmentStart = // @type {Text|Comment} - this._fragmentEnd = null; // @type {Text|Comment} - - // lifecycle state - this._isCompiled = this._isDestroyed = this._isReady = this._isAttached = this._isBeingDestroyed = this._vForRemoving = false; - this._unlinkFn = null; - - // context: - // if this is a transcluded component, context - // will be the common parent vm of this instance - // and its host. - this._context = options._context || this.$parent; - - // scope: - // if this is inside an inline v-for, the scope - // will be the intermediate scope created for this - // repeat fragment. this is used for linking props - // and container directives. - this._scope = options._scope; - - // fragment: - // if this instance is compiled inside a Fragment, it - // needs to reigster itself as a child of that fragment - // for attach/detach to work properly. - this._frag = options._frag; - if (this._frag) { - this._frag.children.push(this); - } - - // push self into parent / transclusion host - if (this.$parent) { - this.$parent.$children.push(this); - } - - // merge options. - options = this.$options = mergeOptions(this.constructor.options, options, this); - - // set ref - this._updateRef(); - - // initialize data as empty object. - // it will be filled up in _initData(). - this._data = {}; - - // call init hook - this._callHook('init'); - - // initialize data observation and scope inheritance. - this._initState(); - - // setup event system and option events. - this._initEvents(); - - // call created hook - this._callHook('created'); - - // if `el` option is passed, start compilation. - if (options.el) { - this.$mount(options.el); - } - }; - } - - var pathCache = new Cache(1000); - - // actions - var APPEND = 0; - var PUSH = 1; - var INC_SUB_PATH_DEPTH = 2; - var PUSH_SUB_PATH = 3; - - // states - var BEFORE_PATH = 0; - var IN_PATH = 1; - var BEFORE_IDENT = 2; - var IN_IDENT = 3; - var IN_SUB_PATH = 4; - var IN_SINGLE_QUOTE = 5; - var IN_DOUBLE_QUOTE = 6; - var AFTER_PATH = 7; - var ERROR = 8; - - var pathStateMachine = []; - - pathStateMachine[BEFORE_PATH] = { - 'ws': [BEFORE_PATH], - 'ident': [IN_IDENT, APPEND], - '[': [IN_SUB_PATH], - 'eof': [AFTER_PATH] - }; - - pathStateMachine[IN_PATH] = { - 'ws': [IN_PATH], - '.': [BEFORE_IDENT], - '[': [IN_SUB_PATH], - 'eof': [AFTER_PATH] - }; - - pathStateMachine[BEFORE_IDENT] = { - 'ws': [BEFORE_IDENT], - 'ident': [IN_IDENT, APPEND] - }; - - pathStateMachine[IN_IDENT] = { - 'ident': [IN_IDENT, APPEND], - '0': [IN_IDENT, APPEND], - 'number': [IN_IDENT, APPEND], - 'ws': [IN_PATH, PUSH], - '.': [BEFORE_IDENT, PUSH], - '[': [IN_SUB_PATH, PUSH], - 'eof': [AFTER_PATH, PUSH] - }; - - pathStateMachine[IN_SUB_PATH] = { - "'": [IN_SINGLE_QUOTE, APPEND], - '"': [IN_DOUBLE_QUOTE, APPEND], - '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH], - ']': [IN_PATH, PUSH_SUB_PATH], - 'eof': ERROR, - 'else': [IN_SUB_PATH, APPEND] - }; - - pathStateMachine[IN_SINGLE_QUOTE] = { - "'": [IN_SUB_PATH, APPEND], - 'eof': ERROR, - 'else': [IN_SINGLE_QUOTE, APPEND] - }; - - pathStateMachine[IN_DOUBLE_QUOTE] = { - '"': [IN_SUB_PATH, APPEND], - 'eof': ERROR, - 'else': [IN_DOUBLE_QUOTE, APPEND] - }; - - /** - * Determine the type of a character in a keypath. - * - * @param {Char} ch - * @return {String} type - */ - - function getPathCharType(ch) { - if (ch === undefined) { - return 'eof'; - } - - var code = ch.charCodeAt(0); - - switch (code) { - case 0x5B: // [ - case 0x5D: // ] - case 0x2E: // . - case 0x22: // " - case 0x27: // ' - case 0x30: - // 0 - return ch; - - case 0x5F: // _ - case 0x24: - // $ - return 'ident'; - - case 0x20: // Space - case 0x09: // Tab - case 0x0A: // Newline - case 0x0D: // Return - case 0xA0: // No-break space - case 0xFEFF: // Byte Order Mark - case 0x2028: // Line Separator - case 0x2029: - // Paragraph Separator - return 'ws'; - } - - // a-z, A-Z - if (code >= 0x61 && code <= 0x7A || code >= 0x41 && code <= 0x5A) { - return 'ident'; - } - - // 1-9 - if (code >= 0x31 && code <= 0x39) { - return 'number'; - } - - return 'else'; - } - - /** - * Format a subPath, return its plain form if it is - * a literal string or number. Otherwise prepend the - * dynamic indicator (*). - * - * @param {String} path - * @return {String} - */ - - function formatSubPath(path) { - var trimmed = path.trim(); - // invalid leading 0 - if (path.charAt(0) === '0' && isNaN(path)) { - return false; - } - return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed; - } - - /** - * Parse a string path into an array of segments - * - * @param {String} path - * @return {Array|undefined} - */ - - function parse(path) { - var keys = []; - var index = -1; - var mode = BEFORE_PATH; - var subPathDepth = 0; - var c, newChar, key, type, transition, action, typeMap; - - var actions = []; - - actions[PUSH] = function () { - if (key !== undefined) { - keys.push(key); - key = undefined; - } - }; - - actions[APPEND] = function () { - if (key === undefined) { - key = newChar; - } else { - key += newChar; - } - }; - - actions[INC_SUB_PATH_DEPTH] = function () { - actions[APPEND](); - subPathDepth++; - }; - - actions[PUSH_SUB_PATH] = function () { - if (subPathDepth > 0) { - subPathDepth--; - mode = IN_SUB_PATH; - actions[APPEND](); - } else { - subPathDepth = 0; - key = formatSubPath(key); - if (key === false) { - return false; - } else { - actions[PUSH](); - } - } - }; - - function maybeUnescapeQuote() { - var nextChar = path[index + 1]; - if (mode === IN_SINGLE_QUOTE && nextChar === "'" || mode === IN_DOUBLE_QUOTE && nextChar === '"') { - index++; - newChar = '\\' + nextChar; - actions[APPEND](); - return true; - } - } - - while (mode != null) { - index++; - c = path[index]; - - if (c === '\\' && maybeUnescapeQuote()) { - continue; - } - - type = getPathCharType(c); - typeMap = pathStateMachine[mode]; - transition = typeMap[type] || typeMap['else'] || ERROR; - - if (transition === ERROR) { - return; // parse error - } - - mode = transition[0]; - action = actions[transition[1]]; - if (action) { - newChar = transition[2]; - newChar = newChar === undefined ? c : newChar; - if (action() === false) { - return; - } - } - - if (mode === AFTER_PATH) { - keys.raw = path; - return keys; - } - } - } - - /** - * External parse that check for a cache hit first - * - * @param {String} path - * @return {Array|undefined} - */ - - function parsePath(path) { - var hit = pathCache.get(path); - if (!hit) { - hit = parse(path); - if (hit) { - pathCache.put(path, hit); - } - } - return hit; - } - - /** - * Get from an object from a path string - * - * @param {Object} obj - * @param {String} path - */ - - function getPath(obj, path) { - return parseExpression(path).get(obj); - } - - /** - * Warn against setting non-existent root path on a vm. - */ - - var warnNonExistent; - if ('development' !== 'production') { - warnNonExistent = function (path, vm) { - warn('You are setting a non-existent path "' + path.raw + '" ' + 'on a vm instance. Consider pre-initializing the property ' + 'with the "data" option for more reliable reactivity ' + 'and better performance.', vm); - }; - } - - /** - * Set on an object from a path - * - * @param {Object} obj - * @param {String | Array} path - * @param {*} val - */ - - function setPath(obj, path, val) { - var original = obj; - if (typeof path === 'string') { - path = parse(path); - } - if (!path || !isObject(obj)) { - return false; - } - var last, key; - for (var i = 0, l = path.length; i < l; i++) { - last = obj; - key = path[i]; - if (key.charAt(0) === '*') { - key = parseExpression(key.slice(1)).get.call(original, original); - } - if (i < l - 1) { - obj = obj[key]; - if (!isObject(obj)) { - obj = {}; - if ('development' !== 'production' && last._isVue) { - warnNonExistent(path, last); - } - set(last, key, obj); - } - } else { - if (isArray(obj)) { - obj.$set(key, val); - } else if (key in obj) { - obj[key] = val; - } else { - if ('development' !== 'production' && obj._isVue) { - warnNonExistent(path, obj); - } - set(obj, key, val); - } - } - } - return true; - } - -var path = Object.freeze({ - parsePath: parsePath, - getPath: getPath, - setPath: setPath - }); - - var expressionCache = new Cache(1000); - - var allowedKeywords = 'Math,Date,this,true,false,null,undefined,Infinity,NaN,' + 'isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,' + 'encodeURIComponent,parseInt,parseFloat'; - var allowedKeywordsRE = new RegExp('^(' + allowedKeywords.replace(/,/g, '\\b|') + '\\b)'); - - // keywords that don't make sense inside expressions - var improperKeywords = 'break,case,class,catch,const,continue,debugger,default,' + 'delete,do,else,export,extends,finally,for,function,if,' + 'import,in,instanceof,let,return,super,switch,throw,try,' + 'var,while,with,yield,enum,await,implements,package,' + 'protected,static,interface,private,public'; - var improperKeywordsRE = new RegExp('^(' + improperKeywords.replace(/,/g, '\\b|') + '\\b)'); - - var wsRE = /\s/g; - var newlineRE = /\n/g; - var saveRE = /[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g; - var restoreRE = /"(\d+)"/g; - var pathTestRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/; - var identRE = /[^\w$\.](?:[A-Za-z_$][\w$]*)/g; - var literalValueRE$1 = /^(?:true|false|null|undefined|Infinity|NaN)$/; - - function noop() {} - - /** - * Save / Rewrite / Restore - * - * When rewriting paths found in an expression, it is - * possible for the same letter sequences to be found in - * strings and Object literal property keys. Therefore we - * remove and store these parts in a temporary array, and - * restore them after the path rewrite. - */ - - var saved = []; - - /** - * Save replacer - * - * The save regex can match two possible cases: - * 1. An opening object literal - * 2. A string - * If matched as a plain string, we need to escape its - * newlines, since the string needs to be preserved when - * generating the function body. - * - * @param {String} str - * @param {String} isString - str if matched as a string - * @return {String} - placeholder with index - */ - - function save(str, isString) { - var i = saved.length; - saved[i] = isString ? str.replace(newlineRE, '\\n') : str; - return '"' + i + '"'; - } - - /** - * Path rewrite replacer - * - * @param {String} raw - * @return {String} - */ - - function rewrite(raw) { - var c = raw.charAt(0); - var path = raw.slice(1); - if (allowedKeywordsRE.test(path)) { - return raw; - } else { - path = path.indexOf('"') > -1 ? path.replace(restoreRE, restore) : path; - return c + 'scope.' + path; - } - } - - /** - * Restore replacer - * - * @param {String} str - * @param {String} i - matched save index - * @return {String} - */ - - function restore(str, i) { - return saved[i]; - } - - /** - * Rewrite an expression, prefixing all path accessors with - * `scope.` and generate getter/setter functions. - * - * @param {String} exp - * @return {Function} - */ - - function compileGetter(exp) { - if (improperKeywordsRE.test(exp)) { - 'development' !== 'production' && warn('Avoid using reserved keywords in expression: ' + exp); - } - // reset state - saved.length = 0; - // save strings and object literal keys - var body = exp.replace(saveRE, save).replace(wsRE, ''); - // rewrite all paths - // pad 1 space here because the regex matches 1 extra char - body = (' ' + body).replace(identRE, rewrite).replace(restoreRE, restore); - return makeGetterFn(body); - } - - /** - * Build a getter function. Requires eval. - * - * We isolate the try/catch so it doesn't affect the - * optimization of the parse function when it is not called. - * - * @param {String} body - * @return {Function|undefined} - */ - - function makeGetterFn(body) { - try { - /* eslint-disable no-new-func */ - return new Function('scope', 'return ' + body + ';'); - /* eslint-enable no-new-func */ - } catch (e) { - if ('development' !== 'production') { - /* istanbul ignore if */ - if (e.toString().match(/unsafe-eval|CSP/)) { - warn('It seems you are using the default build of Vue.js in an environment ' + 'with Content Security Policy that prohibits unsafe-eval. ' + 'Use the CSP-compliant build instead: ' + 'http://vuejs.org/guide/installation.html#CSP-compliant-build'); - } else { - warn('Invalid expression. ' + 'Generated function body: ' + body); - } - } - return noop; - } - } - - /** - * Compile a setter function for the expression. - * - * @param {String} exp - * @return {Function|undefined} - */ - - function compileSetter(exp) { - var path = parsePath(exp); - if (path) { - return function (scope, val) { - setPath(scope, path, val); - }; - } else { - 'development' !== 'production' && warn('Invalid setter expression: ' + exp); - } - } - - /** - * Parse an expression into re-written getter/setters. - * - * @param {String} exp - * @param {Boolean} needSet - * @return {Function} - */ - - function parseExpression(exp, needSet) { - exp = exp.trim(); - // try cache - var hit = expressionCache.get(exp); - if (hit) { - if (needSet && !hit.set) { - hit.set = compileSetter(hit.exp); - } - return hit; - } - var res = { exp: exp }; - res.get = isSimplePath(exp) && exp.indexOf('[') < 0 - // optimized super simple getter - ? makeGetterFn('scope.' + exp) - // dynamic getter - : compileGetter(exp); - if (needSet) { - res.set = compileSetter(exp); - } - expressionCache.put(exp, res); - return res; - } - - /** - * Check if an expression is a simple path. - * - * @param {String} exp - * @return {Boolean} - */ - - function isSimplePath(exp) { - return pathTestRE.test(exp) && - // don't treat literal values as paths - !literalValueRE$1.test(exp) && - // Math constants e.g. Math.PI, Math.E etc. - exp.slice(0, 5) !== 'Math.'; - } - -var expression = Object.freeze({ - parseExpression: parseExpression, - isSimplePath: isSimplePath - }); - - // we have two separate queues: one for directive updates - // and one for user watcher registered via $watch(). - // we want to guarantee directive updates to be called - // before user watchers so that when user watchers are - // triggered, the DOM would have already been in updated - // state. - - var queue = []; - var userQueue = []; - var has = {}; - var circular = {}; - var waiting = false; - - /** - * Reset the batcher's state. - */ - - function resetBatcherState() { - queue.length = 0; - userQueue.length = 0; - has = {}; - circular = {}; - waiting = false; - } - - /** - * Flush both queues and run the watchers. - */ - - function flushBatcherQueue() { - var _again = true; - - _function: while (_again) { - _again = false; - - runBatcherQueue(queue); - runBatcherQueue(userQueue); - // user watchers triggered more watchers, - // keep flushing until it depletes - if (queue.length) { - _again = true; - continue _function; - } - // dev tool hook - /* istanbul ignore if */ - if (devtools && config.devtools) { - devtools.emit('flush'); - } - resetBatcherState(); - } - } - - /** - * Run the watchers in a single queue. - * - * @param {Array} queue - */ - - function runBatcherQueue(queue) { - // do not cache length because more watchers might be pushed - // as we run existing watchers - for (var i = 0; i < queue.length; i++) { - var watcher = queue[i]; - var id = watcher.id; - has[id] = null; - watcher.run(); - // in dev build, check and stop circular updates. - if ('development' !== 'production' && has[id] != null) { - circular[id] = (circular[id] || 0) + 1; - if (circular[id] > config._maxUpdateCount) { - warn('You may have an infinite update loop for watcher ' + 'with expression "' + watcher.expression + '"', watcher.vm); - break; - } - } - } - queue.length = 0; - } - - /** - * Push a watcher into the watcher queue. - * Jobs with duplicate IDs will be skipped unless it's - * pushed when the queue is being flushed. - * - * @param {Watcher} watcher - * properties: - * - {Number} id - * - {Function} run - */ - - function pushWatcher(watcher) { - var id = watcher.id; - if (has[id] == null) { - // push watcher into appropriate queue - var q = watcher.user ? userQueue : queue; - has[id] = q.length; - q.push(watcher); - // queue the flush - if (!waiting) { - waiting = true; - nextTick(flushBatcherQueue); - } - } - } - - var uid$2 = 0; - - /** - * A watcher parses an expression, collects dependencies, - * and fires callback when the expression value changes. - * This is used for both the $watch() api and directives. - * - * @param {Vue} vm - * @param {String|Function} expOrFn - * @param {Function} cb - * @param {Object} options - * - {Array} filters - * - {Boolean} twoWay - * - {Boolean} deep - * - {Boolean} user - * - {Boolean} sync - * - {Boolean} lazy - * - {Function} [preProcess] - * - {Function} [postProcess] - * @constructor - */ - function Watcher(vm, expOrFn, cb, options) { - // mix in options - if (options) { - extend(this, options); - } - var isFn = typeof expOrFn === 'function'; - this.vm = vm; - vm._watchers.push(this); - this.expression = expOrFn; - this.cb = cb; - this.id = ++uid$2; // uid for batching - this.active = true; - this.dirty = this.lazy; // for lazy watchers - this.deps = []; - this.newDeps = []; - this.depIds = new _Set(); - this.newDepIds = new _Set(); - this.prevError = null; // for async error stacks - // parse expression for getter/setter - if (isFn) { - this.getter = expOrFn; - this.setter = undefined; - } else { - var res = parseExpression(expOrFn, this.twoWay); - this.getter = res.get; - this.setter = res.set; - } - this.value = this.lazy ? undefined : this.get(); - // state for avoiding false triggers for deep and Array - // watchers during vm._digest() - this.queued = this.shallow = false; - } - - /** - * Evaluate the getter, and re-collect dependencies. - */ - - Watcher.prototype.get = function () { - this.beforeGet(); - var scope = this.scope || this.vm; - var value; - try { - value = this.getter.call(scope, scope); - } catch (e) { - if ('development' !== 'production' && config.warnExpressionErrors) { - warn('Error when evaluating expression ' + '"' + this.expression + '": ' + e.toString(), this.vm); - } - } - // "touch" every property so they are all tracked as - // dependencies for deep watching - if (this.deep) { - traverse(value); - } - if (this.preProcess) { - value = this.preProcess(value); - } - if (this.filters) { - value = scope._applyFilters(value, null, this.filters, false); - } - if (this.postProcess) { - value = this.postProcess(value); - } - this.afterGet(); - return value; - }; - - /** - * Set the corresponding value with the setter. - * - * @param {*} value - */ - - Watcher.prototype.set = function (value) { - var scope = this.scope || this.vm; - if (this.filters) { - value = scope._applyFilters(value, this.value, this.filters, true); - } - try { - this.setter.call(scope, scope, value); - } catch (e) { - if ('development' !== 'production' && config.warnExpressionErrors) { - warn('Error when evaluating setter ' + '"' + this.expression + '": ' + e.toString(), this.vm); - } - } - // two-way sync for v-for alias - var forContext = scope.$forContext; - if (forContext && forContext.alias === this.expression) { - if (forContext.filters) { - 'development' !== 'production' && warn('It seems you are using two-way binding on ' + 'a v-for alias (' + this.expression + '), and the ' + 'v-for has filters. This will not work properly. ' + 'Either remove the filters or use an array of ' + 'objects and bind to object properties instead.', this.vm); - return; - } - forContext._withLock(function () { - if (scope.$key) { - // original is an object - forContext.rawValue[scope.$key] = value; - } else { - forContext.rawValue.$set(scope.$index, value); - } - }); - } - }; - - /** - * Prepare for dependency collection. - */ - - Watcher.prototype.beforeGet = function () { - Dep.target = this; - }; - - /** - * Add a dependency to this directive. - * - * @param {Dep} dep - */ - - Watcher.prototype.addDep = function (dep) { - var id = dep.id; - if (!this.newDepIds.has(id)) { - this.newDepIds.add(id); - this.newDeps.push(dep); - if (!this.depIds.has(id)) { - dep.addSub(this); - } - } - }; - - /** - * Clean up for dependency collection. - */ - - Watcher.prototype.afterGet = function () { - Dep.target = null; - var i = this.deps.length; - while (i--) { - var dep = this.deps[i]; - if (!this.newDepIds.has(dep.id)) { - dep.removeSub(this); - } - } - var tmp = this.depIds; - this.depIds = this.newDepIds; - this.newDepIds = tmp; - this.newDepIds.clear(); - tmp = this.deps; - this.deps = this.newDeps; - this.newDeps = tmp; - this.newDeps.length = 0; - }; - - /** - * Subscriber interface. - * Will be called when a dependency changes. - * - * @param {Boolean} shallow - */ - - Watcher.prototype.update = function (shallow) { - if (this.lazy) { - this.dirty = true; - } else if (this.sync || !config.async) { - this.run(); - } else { - // if queued, only overwrite shallow with non-shallow, - // but not the other way around. - this.shallow = this.queued ? shallow ? this.shallow : false : !!shallow; - this.queued = true; - // record before-push error stack in debug mode - /* istanbul ignore if */ - if ('development' !== 'production' && config.debug) { - this.prevError = new Error('[vue] async stack trace'); - } - pushWatcher(this); - } - }; - - /** - * Batcher job interface. - * Will be called by the batcher. - */ - - Watcher.prototype.run = function () { - if (this.active) { - var value = this.get(); - if (value !== this.value || - // Deep watchers and watchers on Object/Arrays should fire even - // when the value is the same, because the value may - // have mutated; but only do so if this is a - // non-shallow update (caused by a vm digest). - (isObject(value) || this.deep) && !this.shallow) { - // set new value - var oldValue = this.value; - this.value = value; - // in debug + async mode, when a watcher callbacks - // throws, we also throw the saved before-push error - // so the full cross-tick stack trace is available. - var prevError = this.prevError; - /* istanbul ignore if */ - if ('development' !== 'production' && config.debug && prevError) { - this.prevError = null; - try { - this.cb.call(this.vm, value, oldValue); - } catch (e) { - nextTick(function () { - throw prevError; - }, 0); - throw e; - } - } else { - this.cb.call(this.vm, value, oldValue); - } - } - this.queued = this.shallow = false; - } - }; - - /** - * Evaluate the value of the watcher. - * This only gets called for lazy watchers. - */ - - Watcher.prototype.evaluate = function () { - // avoid overwriting another watcher that is being - // collected. - var current = Dep.target; - this.value = this.get(); - this.dirty = false; - Dep.target = current; - }; - - /** - * Depend on all deps collected by this watcher. - */ - - Watcher.prototype.depend = function () { - var i = this.deps.length; - while (i--) { - this.deps[i].depend(); - } - }; - - /** - * Remove self from all dependencies' subcriber list. - */ - - Watcher.prototype.teardown = function () { - if (this.active) { - // remove self from vm's watcher list - // this is a somewhat expensive operation so we skip it - // if the vm is being destroyed or is performing a v-for - // re-render (the watcher list is then filtered by v-for). - if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) { - this.vm._watchers.$remove(this); - } - var i = this.deps.length; - while (i--) { - this.deps[i].removeSub(this); - } - this.active = false; - this.vm = this.cb = this.value = null; - } - }; - - /** - * Recrusively traverse an object to evoke all converted - * getters, so that every nested property inside the object - * is collected as a "deep" dependency. - * - * @param {*} val - */ - - var seenObjects = new _Set(); - function traverse(val, seen) { - var i = undefined, - keys = undefined; - if (!seen) { - seen = seenObjects; - seen.clear(); - } - var isA = isArray(val); - var isO = isObject(val); - if ((isA || isO) && Object.isExtensible(val)) { - if (val.__ob__) { - var depId = val.__ob__.dep.id; - if (seen.has(depId)) { - return; - } else { - seen.add(depId); - } - } - if (isA) { - i = val.length; - while (i--) traverse(val[i], seen); - } else if (isO) { - keys = Object.keys(val); - i = keys.length; - while (i--) traverse(val[keys[i]], seen); - } - } - } - - var text$1 = { - - bind: function bind() { - this.attr = this.el.nodeType === 3 ? 'data' : 'textContent'; - }, - - update: function update(value) { - this.el[this.attr] = _toString(value); - } - }; - - var templateCache = new Cache(1000); - var idSelectorCache = new Cache(1000); - - var map = { - efault: [0, '', ''], - legend: [1, '
', '
'], - tr: [2, '', '
'], - col: [2, '', '
'] - }; - - map.td = map.th = [3, '', '
']; - - map.option = map.optgroup = [1, '']; - - map.thead = map.tbody = map.colgroup = map.caption = map.tfoot = [1, '', '
']; - - map.g = map.defs = map.symbol = map.use = map.image = map.text = map.circle = map.ellipse = map.line = map.path = map.polygon = map.polyline = map.rect = [1, '', '']; - - /** - * Check if a node is a supported template node with a - * DocumentFragment content. - * - * @param {Node} node - * @return {Boolean} - */ - - function isRealTemplate(node) { - return isTemplate(node) && isFragment(node.content); - } - - var tagRE$1 = /<([\w:-]+)/; - var entityRE = /&#?\w+?;/; - var commentRE = /