diff options
author | Douwe Maan <douwe@gitlab.com> | 2015-12-10 16:04:34 +0300 |
---|---|---|
committer | Douwe Maan <douwe@gitlab.com> | 2015-12-10 16:04:34 +0300 |
commit | 10387f6b8a9071688d7db9a12c910ca02660ca87 (patch) | |
tree | 211b55d18305793aa76fa6260ff25ca1b74dc36e /app | |
parent | bcd89a58e736685bcce662fe0acf793ee925b536 (diff) | |
parent | bdc62d704c79b8f4e39dc7b5660b8d657a434895 (diff) |
Merge branch 'master' into tmp-reference-pipeline-and-caching
# Conflicts:
# spec/lib/gitlab/markdown/autolink_filter_spec.rb
# spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
# spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
# spec/lib/gitlab/markdown/cross_project_reference_spec.rb
# spec/lib/gitlab/markdown/emoji_filter_spec.rb
# spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb
# spec/lib/gitlab/markdown/external_link_filter_spec.rb
# spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
# spec/lib/gitlab/markdown/label_reference_filter_spec.rb
# spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
# spec/lib/gitlab/markdown/redactor_filter_spec.rb
# spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb
# spec/lib/gitlab/markdown/relative_link_filter_spec.rb
# spec/lib/gitlab/markdown/sanitization_filter_spec.rb
# spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
# spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb
# spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb
# spec/lib/gitlab/markdown/task_list_filter_spec.rb
# spec/lib/gitlab/markdown/upload_link_filter_spec.rb
# spec/lib/gitlab/markdown/user_reference_filter_spec.rb
Diffstat (limited to 'app')
63 files changed, 696 insertions, 326 deletions
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index 593a8f42130..b0eeb1db536 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -43,6 +43,7 @@ # class @MergeRequestTabs diffsLoaded: false + buildsLoaded: false commitsLoaded: false constructor: (@opts = {}) -> @@ -54,6 +55,12 @@ class @MergeRequestTabs 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) @@ -63,6 +70,8 @@ class @MergeRequestTabs @loadCommits($target.attr('href')) else if action == 'diffs' @loadDiff($target.attr('href')) + else if action == 'builds' + @loadBuilds($target.attr('href')) @setCurrentAction(action) @@ -101,7 +110,7 @@ class @MergeRequestTabs action = 'notes' if action == 'show' # Remove a trailing '/commits' or '/diffs' - new_state = @_location.pathname.replace(/\/(commits|diffs)(\.html)?\/?$/, '') + new_state = @_location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '') # Append the new action if we're on a tab other than 'notes' unless action == 'notes' @@ -139,6 +148,17 @@ class @MergeRequestTabs @diffsLoaded = true @scrollToElement("#diffs") + loadBuilds: (source) -> + return if @buildsLoaded + + @_get + url: "#{source}.json" + success: (data) => + document.getElementById('builds').innerHTML = data.html + $('.js-timeago').timeago() + @buildsLoaded = true + @scrollToElement("#builds") + # Show or hide the loading spinner # # status - Boolean, true to show, false to hide diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 927641216e4..cc48f8c8166 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -7,7 +7,7 @@ padding: 0; list-style: none; - li { + > li { padding: 10px 15px; min-height: 20px; border-bottom: 1px solid #eee; @@ -88,8 +88,14 @@ ul.bordered-list { } } -li.task-list-item { - list-style-type: none; +ul.task-list { + li.task-list-item { + list-style-type: none; + } + + ul:not(.task-list) { + padding-left: 1.3em; + } } ul.content-list { @@ -125,3 +131,27 @@ ul.content-list { margin: 0; } } + +ul.controls { + padding-top: 1px; + float: right; + list-style: none; + + .btn { + padding: 10px 14px; + } + + > li { + float: left; + padding-right: 10px; + + .author_link { + display: inline-block; + + .avatar-inline { + margin-left: 0; + margin-right: 0; + } + } + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 6a1d3bd19d3..fc8c7161991 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -18,6 +18,7 @@ .accept-merge-holder { .accept-action { display: inline-block; + float: left; .accept_merge_request { &.ci-pending, @@ -36,14 +37,15 @@ .accept-control { display: inline-block; + float: left; margin: 0; margin-left: 20px; padding: 5px; + padding-top: 12px; line-height: 20px; &.right { float: right; - padding-top: 12px; a { color: $gl-gray; } @@ -81,6 +83,10 @@ &.ci-error { color: $gl-danger; } + + a.monospace { + color: inherit; + } } .mr-widget-body, diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 1980fe0d458..4dff87abaa4 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -109,13 +109,9 @@ ul.notes { } } - // Reduce left padding of first task list ul element - ul.task-list:first-child { - padding-left: 10px; - - // sub-tasks should be padded normally - ul { - padding-left: 20px; + ul.task-list { + ul:not(.task-list) { + padding-left: 1.3em; } } diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 2025158d065..f74daff3bd0 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -40,7 +40,9 @@ class PasswordsController < Devise::PasswordsController def throttle_reset return unless resource && resource.recently_sent_password_reset? - redirect_to new_password_path(resource_name), - alert: I18n.t('devise.passwords.recently_reset') + # Throttle reset attempts, but return a normal message to + # avoid user enumeration attack. + redirect_to new_user_session_path, + notice: I18n.t('devise.passwords.send_paranoid_instructions') end end diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index c2aaf094e68..7d0d57858e0 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -21,7 +21,7 @@ class Projects::ApplicationController < ApplicationController unless @repository.branch_names.include?(@ref) redirect_to( namespace_project_tree_path(@project.namespace, @project, @ref), - notice: "This action is not allowed unless you are on top of a branch" + notice: "This action is not allowed unless you are on a branch" ) end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 31a33bfd237..62163682936 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -162,12 +162,20 @@ class Projects::BlobController < Projects::ApplicationController end def sanitized_new_branch_name - @new_branch ||= sanitize(strip_tags(params[:new_branch])) + sanitize(strip_tags(params[:new_branch])) end def editor_variables @current_branch = @ref - @new_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref + + @new_branch = + if params[:new_branch].present? + sanitized_new_branch_name + elsif ::Gitlab::GitAccess.new(current_user, @project).can_push_to_branch?(@ref) + @ref + else + @repository.next_patch_branch + end @file_path = if action_name.to_s == 'create' diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 3f137440e28..e8af205b788 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -37,7 +37,7 @@ class Projects::CommitController < Projects::ApplicationController def cancel_builds ci_commit.builds.running_or_pending.each(&:cancel) - redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha) + redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) end def retry_builds @@ -47,7 +47,7 @@ class Projects::CommitController < Projects::ApplicationController end end - redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.sha) + redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha) end def branches @@ -74,8 +74,8 @@ class Projects::CommitController < Projects::ApplicationController end @notes_count = commit.notes.count - - @builds = ci_commit.builds if ci_commit + + @statuses = ci_commit.statuses if ci_commit end def authorize_manage_builds! diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index c5fb49de46a..530f3d3dcb8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,13 +1,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :merge, :merge_check, - :ci_status, :toggle_subscription + :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check, + :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds ] - before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits] - before_action :validates_merge_request, only: [:show, :diffs, :commits] - before_action :define_show_vars, only: [:show, :diffs, :commits] - before_action :ensure_ref_fetched, only: [:show, :commits, :diffs] + before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds] + before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds] + before_action :define_show_vars, only: [:show, :diffs, :commits, :builds] + before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds] + before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds] # Allow read any merge_request before_action :authorize_read_merge_request! @@ -79,6 +80,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def builds + @ci_project = @merge_request.source_project.gitlab_ci_project + + respond_to do |format| + format.html { render 'show' } + format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } } + end + end + def new params[:merge_request] ||= ActionController::Parameters.new(source_project: @project) @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute @@ -91,20 +101,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController @target_project = merge_request.target_project @source_project = merge_request.source_project - @commits = @merge_request.compare_commits + @commits = @merge_request.compare_commits.reverse @commit = @merge_request.last_commit @first_commit = @merge_request.first_commit @diffs = @merge_request.compare_diffs + + @ci_project = @source_project.gitlab_ci_project + @ci_commit = @merge_request.ci_commit + @statuses = @ci_commit.statuses if @ci_commit + @note_counts = Note.where(commit_id: @commits.map(&:id)). group(:commit_id).count end - def edit - @source_project = @merge_request.source_project - @target_project = @merge_request.target_project - @target_branches = @merge_request.target_project.repository.branch_names - end - def create @target_branches ||= [] @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute @@ -118,6 +127,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def edit + @source_project = @merge_request.source_project + @target_project = @merge_request.target_project + @target_branches = @merge_request.target_project.repository.branch_names + end + def update @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) @@ -150,15 +165,29 @@ class Projects::MergeRequestsController < Projects::ApplicationController render partial: "projects/merge_requests/widget/show.html.haml", layout: false end + def cancel_merge_when_build_succeeds + return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + + MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request) + end + def merge return access_denied! unless @merge_request.can_be_merged_by?(current_user) - if @merge_request.mergeable? - @merge_request.update(merge_error: nil) - MergeWorker.perform_async(@merge_request.id, current_user.id, params) - @status = true + unless @merge_request.mergeable? + @status = :failed + return + end + + @merge_request.update(merge_error: nil) + + if params[:merge_when_build_succeeds] && @merge_request.ci_commit && @merge_request.ci_commit.active? + MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params) + .execute(@merge_request) + @status = :merge_when_build_succeeds else - @status = false + MergeWorker.perform_async(@merge_request.id, current_user.id, params) + @status = :success end end @@ -265,6 +294,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController @merge_request_diff = @merge_request.merge_request_diff @ci_commit = @merge_request.ci_commit + @statuses = @ci_commit.statuses if @ci_commit if @merge_request.locked_long_ago? @merge_request.unlock_mr @@ -272,6 +302,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + def define_widget_vars + @ci_commit = @merge_request.ci_commit + end + def invalid_mr # Render special view for MR with removed source or target branch render 'invalid' @@ -285,6 +319,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController ) end + def merge_params + params.permit(:should_remove_source_branch, :commit_message) + end + # Make sure merge requests created before 8.0 # have head file in refs/merge-requests/ def ensure_ref_fetched diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 07eb94e4f48..8364fc293b7 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -23,7 +23,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_members = @group_members.where(user_id: users) end - @group_members = @group_members.order('access_level DESC').limit(20) + @group_members = @group_members.order('access_level DESC') end @project_member = @project.project_members.new diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 4a3d971f7c6..68e5d5be600 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -30,26 +30,24 @@ module BlobHelper nil end - if blob_viewable?(blob) - text = 'Edit' - after = options[:after] || '' - from_mr = options[:from_merge_request_id] - link_opts = {} - link_opts[:from_merge_request_id] = from_mr if from_mr - cls = 'btn btn-small' - if allowed_tree_edit?(project, ref) - link_to(text, - namespace_project_edit_blob_path(project.namespace, project, - tree_join(ref, path), - link_opts), - class: cls - ) - else - content_tag :span, text, class: cls + ' disabled' - end + after.html_safe - else - '' - end + return unless blob && blob.text? && blob_editable?(blob) + + text = 'Edit' + after = options[:after] || '' + from_mr = options[:from_merge_request_id] + link_opts = {} + link_opts[:from_merge_request_id] = from_mr if from_mr + cls = 'btn btn-small' + link_to(text, + namespace_project_edit_blob_path(project.namespace, project, + tree_join(ref, path), + link_opts), + class: cls + ) + after.html_safe + end + + def blob_editable?(blob, project = @project, ref = @ref) + !blob.lfs_pointer? && allowed_tree_edit?(project, ref) end def leave_edit_message diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index d6eaa7d57bc..e39548e17e1 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -11,7 +11,7 @@ module BranchesHelper def can_push_branch?(project, branch_name) return false unless project.repository.branch_names.include?(branch_name) - + ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name) end end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 70f8c9ae221..8e1f8f9ba6d 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -58,7 +58,7 @@ module CiStatusHelper def render_ci_status(ci_commit) link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}", - title: "Build status: #{ci_status_label(ci_commit)}", + title: "Build #{ci_status_label(ci_commit)}", data: { toggle: 'tooltip', placement: 'left' } do ci_status_icon(ci_commit) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 48729e5260e..d061136b7b8 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -175,11 +175,19 @@ module ProjectsHelper end def default_url_to_repo(project = @project) - current_user ? project.url_to_repo : project.http_url_to_repo + if default_clone_protocol == "ssh" + project.ssh_url_to_repo + else + project.http_url_to_repo + end end def default_clone_protocol - current_user ? "ssh" : "http" + if !current_user || current_user.require_ssh_key? + "http" + else + "ssh" + end end def project_last_activity(project) diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 6afa1aacc5b..886a1e734b5 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -46,16 +46,26 @@ module TreeHelper File.join(*args) end + def on_top_of_branch?(project = @project, ref = @ref) + project.repository.branch_names.include?(ref) + end + def allowed_tree_edit?(project = nil, ref = nil) project ||= @project ref ||= @ref - return false unless project.repository.branch_names.include?(ref) + return false unless on_top_of_branch?(project, ref) - ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref) + can?(current_user, :push_code, project) end - def can_delete_or_replace?(blob) - allowed_tree_edit? && !blob.lfs_pointer? + def tree_edit_branch(project = @project, ref = @ref) + if allowed_tree_edit?(project, ref) + if can_push_branch?(project, ref) + ref + else + project.repository.next_patch_branch + end + end end def tree_breadcrumbs(tree, max_links = 2) diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 72c65030f94..2e69ce923a2 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -12,22 +12,22 @@ module VisibilityLevelHelper # Return the description for the +level+ argument. # - # +level+ One of the Gitlab::VisibilityLevel constants - # +form_model+ Either a model object (Project, Snippet, etc.) or the name of - # a Project or Snippet class. + # +level+ One of the Gitlab::VisibilityLevel constants + # +form_model+ Either a model object (Project, Snippet, etc.) or the name of + # a Project or Snippet class. def visibility_level_description(level, form_model) - case form_model.is_a?(String) ? form_model : form_model.class.name - when 'PersonalSnippet', 'ProjectSnippet', 'Snippet' - snippet_visibility_level_description(level) - when 'Project' + case form_model + when Project project_visibility_level_description(level) + when Snippet + snippet_visibility_level_description(level, form_model) end end def project_visibility_level_description(level) case level when Gitlab::VisibilityLevel::PRIVATE - "Project access must be granted explicitly for each user." + "Project access must be granted explicitly to each user." when Gitlab::VisibilityLevel::INTERNAL "The project can be cloned by any logged in user." when Gitlab::VisibilityLevel::PUBLIC @@ -35,12 +35,16 @@ module VisibilityLevelHelper end end - def snippet_visibility_level_description(level) + def snippet_visibility_level_description(level, snippet = nil) case level when Gitlab::VisibilityLevel::PRIVATE - "The snippet is visible only for me." + if snippet.is_a? ProjectSnippet + "The snippet is visible only to project members." + else + "The snippet is visible only to me." + end when Gitlab::VisibilityLevel::INTERNAL - "The snippet is visible for any logged in user." + "The snippet is visible to any logged in user." when Gitlab::VisibilityLevel::PUBLIC "The snippet can be accessed without any authentication." end diff --git a/app/models/ability.rb b/app/models/ability.rb index 07f3a56ec7a..cd5ae0fb0fd 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -346,12 +346,10 @@ class Ability unless group.last_owner?(target_user) can_manage = group_abilities(user, group).include?(:admin_group_member) - if can_manage && user != target_user + if can_manage rules << :update_group_member rules << :destroy_group_member - end - - if user == target_user + elsif user == target_user rules << :destroy_group_member end end @@ -367,12 +365,10 @@ class Ability unless target_user == project.owner can_manage = project_abilities(user, project).include?(:admin_project_member) - if can_manage && user != target_user + if can_manage rules << :update_project_member rules << :destroy_project_member - end - - if user == target_user + elsif user == target_user rules << :destroy_project_member end end diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb index cb90b0de63d..75465685e98 100644 --- a/app/models/ci/commit.rb +++ b/app/models/ci/commit.rb @@ -165,6 +165,14 @@ module Ci status == 'canceled' end + def active? + running? || pending? + end + + def complete? + canceled? || success? || failed? + end + def duration duration_array = latest_statuses.map(&:duration).compact duration_array.reduce(:+).to_i diff --git a/app/models/commit.rb b/app/models/commit.rb index ac22fc38ace..0ba7b584d91 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -175,11 +175,11 @@ class Commit end def author - @author ||= User.find_by_any_email(author_email) + @author ||= User.find_by_any_email(author_email.downcase) end def committer - @committer ||= User.find_by_any_email(committer_email) + @committer ||= User.find_by_any_email(committer_email.downcase) end def parents diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index e70f4d37184..ff619965a57 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -1,34 +1,30 @@ # == Schema Information # -# Table name: ci_builds -# -# id :integer not null, primary key -# project_id :integer -# status :string(255) -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# coverage :float -# commit_id :integer -# commands :text -# job_id :integer -# name :string(255) -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string(255) -# trigger_request_id :integer -# stage_idx :integer -# tag :boolean -# ref :string(255) -# user_id :integer -# type :string(255) -# target_url :string(255) -# description :string(255) -# artifacts_file :text +# project_id integer +# status string +# finished_at datetime +# trace text +# created_at datetime +# updated_at datetime +# started_at datetime +# runner_id integer +# coverage float +# commit_id integer +# commands text +# job_id integer +# name string +# deploy boolean default: false +# options text +# allow_failure boolean default: false, null: false +# stage string +# trigger_request_id integer +# stage_idx integer +# tag boolean +# ref string +# user_id integer +# type string +# target_url string +# description string # class CommitStatus < ActiveRecord::Base @@ -79,6 +75,10 @@ class CommitStatus < ActiveRecord::Base build.update_attributes finished_at: Time.now end + after_transition [:pending, :running] => :success do |build, transition| + MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.gl_project, nil).trigger(build) + end + state :pending, value: 'pending' state :running, value: 'running' state :failed, value: 'failed' diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb index 18657c3e1c8..86b1b7e2f99 100644 --- a/app/models/lfs_object.rb +++ b/app/models/lfs_object.rb @@ -1,3 +1,15 @@ +# == Schema Information +# +# Table name: lfs_objects +# +# id :integer not null, primary key +# oid :string(255) not null +# size :integer not null +# created_at :datetime +# updated_at :datetime +# file :string(255) +# + class LfsObject < ActiveRecord::Base has_many :lfs_objects_projects, dependent: :destroy has_many :projects, through: :lfs_objects_projects diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb index 0fd5f089db9..890736bfc80 100644 --- a/app/models/lfs_objects_project.rb +++ b/app/models/lfs_objects_project.rb @@ -1,3 +1,14 @@ +# == Schema Information +# +# Table name: lfs_objects_projects +# +# id :integer not null, primary key +# lfs_object_id :integer not null +# project_id :integer not null +# created_at :datetime +# updated_at :datetime +# + class LfsObjectsProject < ActiveRecord::Base belongs_to :project belongs_to :lfs_object diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 080b7f7fb88..f6f77a16267 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -2,25 +2,28 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime +# updated_at :datetime +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer +# description :text +# position :integer default(0) +# locked_at :datetime +# updated_by_id :integer +# merge_error :string(255) +# merge_params :text (serialized to hash) +# merge_when_build_succeeds :boolean default(false), not null +# merge_user_id :integer # require Rails.root.join("app/models/commit") @@ -35,9 +38,12 @@ class MergeRequest < ActiveRecord::Base belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project" belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project" + belongs_to :merge_user, class_name: "User" has_one :merge_request_diff, dependent: :destroy + serialize :merge_params, Hash + after_create :create_merge_request_diff after_update :update_merge_request_diff @@ -121,6 +127,7 @@ class MergeRequest < ActiveRecord::Base validates :source_branch, presence: true validates :target_project, presence: true validates :target_branch, presence: true + validates :merge_user, presence: true, if: :merge_when_build_succeeds? validate :validate_branches validate :validate_fork @@ -258,6 +265,16 @@ class MergeRequest < ActiveRecord::Base end end + def can_cancel_merge_when_build_succeeds?(current_user) + can_be_merged_by?(current_user) || self.author == current_user + end + + def can_remove_source_branch?(current_user) + !source_project.protected_branch?(source_branch) && + !source_project.root_ref?(source_branch) && + Ability.abilities.allowed?(current_user, :push_code, source_project) + end + def mr_and_commit_notes # Fetch comments only from last 100 commits commits_for_notes_limit = 100 @@ -393,6 +410,16 @@ class MergeRequest < ActiveRecord::Base message end + def reset_merge_when_build_succeeds + return unless merge_when_build_succeeds? + + self.merge_when_build_succeeds = false + self.merge_user = nil + self.merge_params = nil + + self.save + end + # Return array of possible target branches # depends on target project of MR def target_branches @@ -480,8 +507,10 @@ class MergeRequest < ActiveRecord::Base end def ci_commit - if last_commit and source_project - source_project.ci_commit(last_commit.id) - end + @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project + end + + def broken? + self.commits.blank? || branch_missing? || cannot_be_merged? end end diff --git a/app/models/note.rb b/app/models/note.rb index 8f7ff75d0d2..de9392adbf4 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -16,6 +16,7 @@ # system :boolean default(FALSE), not null # st_diff :text # updated_by_id :integer +# is_award :boolean default(FALSE), not null # require 'carrierwave/orm/activerecord' diff --git a/app/models/project.rb b/app/models/project.rb index 9c28f782cdf..e78868af1cc 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -28,6 +28,7 @@ # import_type :string(255) # import_source :string(255) # commit_count :integer default(0) +# import_error :text # require 'carrierwave/orm/activerecord' diff --git a/app/models/repository.rb b/app/models/repository.rb index 1d43307e1e7..1edec52c09e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -329,6 +329,17 @@ class Repository commit(sha) end + def next_patch_branch + patch_branch_ids = self.branch_names.map do |n| + result = n.match(/\Apatch-([0-9]+)\z/) + result[1].to_i if result + end.compact + + highest_patch_branch_id = patch_branch_ids.max || 0 + + "patch-#{highest_patch_branch_id + 1}" + end + # Remove archives older than 2 hours def branches_sorted_by(value) case value diff --git a/app/models/user.rb b/app/models/user.rb index cfed797e725..7155dd2bea7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -56,6 +56,7 @@ # project_view :integer default(0) # consumed_timestep :integer # layout :integer default(0) +# hide_project_limit :boolean default(FALSE) # require 'carrierwave/orm/activerecord' diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index f50aaf2eb52..9a67b160940 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -53,7 +53,7 @@ module Files unless project.empty_repo? unless repository.branch_names.include?(@current_branch) - raise_error("You can only create files if you are on top of a branch") + raise_error("You can only create or edit files when you are on a branch") end if @current_branch != @target_branch diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index d619b72e3c2..cabc3d8fabb 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,15 +6,12 @@ module MergeRequests # Executed when you do merge via GitLab UI # class MergeService < MergeRequests::BaseService - attr_reader :merge_request, :commit_message + attr_reader :merge_request - def execute(merge_request, commit_message) - @commit_message = commit_message + def execute(merge_request) @merge_request = merge_request - unless @merge_request.mergeable? - return error('Merge request is not mergeable') - end + return error('Merge request is not mergeable') unless @merge_request.mergeable? merge_request.in_locked_state do if commit @@ -32,7 +29,7 @@ module MergeRequests committer = repository.user_to_committer(current_user) options = { - message: commit_message, + message: params[:commit_message] || merge_request.merge_commit_message, author: committer, committer: committer } @@ -46,6 +43,11 @@ module MergeRequests def after_merge MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) + + if params[:should_remove_source_branch] + DeleteBranchService.new(@merge_request.source_project, current_user). + execute(merge_request.source_branch) + end end end end diff --git a/app/services/merge_requests/merge_when_build_succeeds_service.rb b/app/services/merge_requests/merge_when_build_succeeds_service.rb new file mode 100644 index 00000000000..5cf7404a493 --- /dev/null +++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb @@ -0,0 +1,55 @@ +module MergeRequests + class MergeWhenBuildSucceedsService < MergeRequests::BaseService + # Marks the passed `merge_request` to be merged when the build succeeds or + # updates the params for the automatic merge + def execute(merge_request) + merge_request.merge_params.merge!(params) + + # The service is also called when the merge params are updated. + already_approved = merge_request.merge_when_build_succeeds? + + unless already_approved + merge_request.merge_when_build_succeeds = true + merge_request.merge_user = @current_user + + SystemNoteService.merge_when_build_succeeds(merge_request, @project, @current_user, merge_request.last_commit) + end + + merge_request.save + end + + # Triggers the automatic merge of merge_request once the build succeeds + def trigger(build) + merge_requests = merge_request_from(build) + + merge_requests.each do |merge_request| + next unless merge_request.merge_when_build_succeeds? + + if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable? + MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params) + end + end + end + + # Cancels the automatic merge + def cancel(merge_request) + if merge_request.merge_when_build_succeeds? && merge_request.open? + merge_request.reset_merge_when_build_succeeds + SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user) + + success + else + error("Can't cancel the automatic merge", 406) + end + end + + private + + def merge_request_from(build) + merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a + merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a + + merge_requests.uniq.select(&:source_project) + end + end +end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index e180edb4bf3..b26c7513f5b 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -11,6 +11,7 @@ module MergeRequests # empty diff during a manual merge close_merge_requests reload_merge_requests + reset_merge_when_build_succeeds # Leave a system note if a branch was deleted/added if branch_added? || branch_removed? @@ -57,7 +58,6 @@ module MergeRequests merge_requests = filter_merge_requests(merge_requests) merge_requests.each do |merge_request| - if merge_request.source_branch == @branch_name || force_push? merge_request.reload_code merge_request.mark_as_unchecked @@ -76,6 +76,10 @@ module MergeRequests end end + def reset_merge_when_build_succeeds + merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds) + end + def find_new_commits if branch_added? @commits = [] diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 09c159510cd..6975b2ee55b 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -130,6 +130,20 @@ class SystemNoteService create_note(noteable: noteable, project: project, author: author, note: body) end + # Called when 'merge when build succeeds' is executed + def self.merge_when_build_succeeds(noteable, project, author, last_commit) + body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + + # Called when 'merge when build succeeds' is canceled + def self.cancel_merge_when_build_succeeds(noteable, project, author) + body = "Canceled the automatic merge" + + create_note(noteable: noteable, project: project, author: author, note: body) + end + # Called when the title of a Noteable is changed # # noteable - Noteable object that responds to `title` diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index ddaf0e0e8ff..6c355366948 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -14,11 +14,11 @@ .form-group.project-visibility-level-holder = f.label :default_project_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project') + = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project) .form-group.project-visibility-level-holder = f.label :default_snippet_visibility, class: 'control-label col-sm-2' .col-sm-10 - = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet') + = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: PersonalSnippet) .form-group = f.label :restricted_visibility_levels, class: 'control-label col-sm-2' .col-sm-10 diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 87a7707b095..2fcba7bd672 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -32,7 +32,7 @@ Files - if project_nav_tab? :commits - = nav_link(controller: %w(commit commits compare repositories tags branches releases)) do + = nav_link(controller: %w(commit commits compare repositories tags branches releases network)) do = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do = icon('history fw') %span @@ -46,13 +46,6 @@ Builds %span.count.builds_counter= @project.ci_builds.running_or_pending.count(:all) - - if project_nav_tab? :network - = nav_link(controller: %w(network)) do - = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do - = icon('code-fork fw') - %span - Network - - if project_nav_tab? :graphs = nav_link(controller: %w(graphs)) do = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs' do @@ -118,3 +111,10 @@ = icon('cogs fw') %span Settings + + -# Global shortcut to network page for compatibility + - if project_nav_tab? :network + %li.hidden + = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do + Network + diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 0e54e59e953..b1df8d19938 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,5 +1,4 @@ .btn-group.tree-btn-group - = edit_blob_link(@project, @ref, @path) = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' -# only show normal/blame view links for text files @@ -12,11 +11,16 @@ class: 'btn btn-sm' unless @blob.empty? = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'btn btn-sm' - - if @ref != @commit.sha - = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, - tree_join(@commit.sha, @path)), class: 'btn btn-sm' + = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project, + tree_join(@commit.sha, @path)), class: 'btn btn-sm' -- if can_delete_or_replace?(@blob) +- if blob_editable?(@blob) .btn-group{ role: "group" } + = edit_blob_link(@project, @ref, @path) %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Delete +- elsif !on_top_of_branch? + .btn-group{ role: "group" } + %button.btn.btn-default.disabled.has_tooltip{title: "You can only edit files when you are on a branch.", data: {container: 'body'}} Edit + %button.btn.btn-default.disabled.has_tooltip{title: "You can only replace files when you are on a branch.", data: {container: 'body'}} Replace + %button.btn.btn-remove.disabled.has_tooltip{title: "You can only delete files when you are on a branch.", data: {container: 'body'}} Delete diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml index 09d6fc18e3e..3f8d11ed8c8 100644 --- a/app/views/projects/blob/show.html.haml +++ b/app/views/projects/blob/show.html.haml @@ -6,7 +6,7 @@ %div#tree-holder.tree-holder = render 'blob', blob: @blob -- if can_delete_or_replace?(@blob) +- if blob_editable?(@blob) = render 'projects/blob/remove' - title = "Replace #{@blob.name}" diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 742676305a9..fbf2c293db8 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -12,17 +12,20 @@ %li{class: ('active' if @scope.nil?)} = link_to project_builds_path(@project) do Running - %span.badge.js-running-count= @all_builds.running_or_pending.count(:id) + %span.badge.js-running-count + = number_with_delimiter(@all_builds.running_or_pending.count(:id)) %li{class: ('active' if @scope == 'finished')} = link_to project_builds_path(@project, scope: :finished) do Finished - %span.badge.js-running-count= @all_builds.finished.count(:id) + %span.badge.js-running-count + = number_with_delimiter(@all_builds.finished.count(:id)) %li{class: ('active' if @scope == 'all')} = link_to project_builds_path(@project, scope: :all) do All - %span.badge.js-totalbuilds-count= @all_builds.count(:id) + %span.badge.js-totalbuilds-count + = number_with_delimiter(@all_builds.count(:id)) .gray-content-block #{(@scope || 'running').capitalize} builds from this project diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml new file mode 100644 index 00000000000..e4d81182c1a --- /dev/null +++ b/app/views/projects/commit/_builds.html.haml @@ -0,0 +1,67 @@ +.gray-content-block.middle-block + .pull-right + - if @ci_project && can?(current_user, :manage_builds, @ci_commit.gl_project) + - if @ci_commit.builds.latest.failed.any?(&:retryable?) + = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.gl_project.namespace, @ci_commit.gl_project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post + + - if @ci_commit.builds.running_or_pending.any? + = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@ci_commit.gl_project.namespace, @ci_commit.gl_project, @ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post + + .oneline + = pluralize @statuses.count(:id), "build" + - if defined?(link_to_commit) && link_to_commit + for commit + = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.gl_project.namespace, @ci_commit.gl_project, @ci_commit.sha), class: "monospace" + - if @ci_commit.duration > 0 + in + = time_interval_in_words @ci_commit.duration + +- if @ci_commit.yaml_errors.present? + .bs-callout.bs-callout-danger + %h4 Found errors in your .gitlab-ci.yml: + %ul + - @ci_commit.yaml_errors.split(",").each do |error| + %li= error + +- if @ci_commit.gl_project.builds_enabled? && !@ci_commit.ci_yaml_file + .bs-callout.bs-callout-warning + \.gitlab-ci.yml not found in this commit + +.table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @ci_project && @ci_project.coverage_enabled? + %th Coverage + %th + - @ci_commit.refs.each do |ref| + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, + locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true, allow_retry: true } + +- if @ci_commit.retried.any? + .gray-content-block.second-block + Retried builds + + .table-holder + %table.table.builds + %thead + %tr + %th Status + %th Build ID + %th Ref + %th Stage + %th Name + %th Duration + %th Finished at + - if @ci_project && @ci_project.coverage_enabled? + %th Coverage + %th + = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, + locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true } diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index 76dc87a8824..f74f8b427ec 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -6,4 +6,4 @@ = nav_link(path: 'commit#builds') do = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do Builds - %span.badge= @builds.count(:id) + %span.badge= @statuses.count diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml index 00cf9c76102..99d62503a94 100644 --- a/app/views/projects/commit/builds.html.haml +++ b/app/views/projects/commit/builds.html.haml @@ -3,70 +3,4 @@ = render "commit_box" = render "ci_menu" - -- if @ci_commit.yaml_errors.present? - .bs-callout.bs-callout-danger - %h4 Found errors in your .gitlab-ci.yml: - %ul - - @ci_commit.yaml_errors.split(",").each do |error| - %li= error - -- unless @ci_commit.ci_yaml_file - .bs-callout.bs-callout-warning - \.gitlab-ci.yml not found in this commit - -.gray-content-block.second-block - Latest builds - - .pull-right - - if @ci_commit.duration > 0 - %i.fa.fa-time - #{time_interval_in_words @ci_commit.duration} - - - - - if @ci_project && current_user && can?(current_user, :manage_builds, @project) - - if @ci_commit.builds.latest.failed.any?(&:retryable?) - = link_to "Retry failed", retry_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-primary', method: :post - - - if @ci_commit.builds.running_or_pending.any? - = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-danger', method: :post - -.table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_project && @ci_project.coverage_enabled? - %th Coverage - %th - - @ci_commit.refs.each do |ref| - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered, - locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true, allow_retry: true } - -- if @ci_commit.retried.any? - .gray-content-block.second-block - Retried builds - - .table-holder - %table.table.builds - %thead - %tr - %th Status - %th Build ID - %th Ref - %th Stage - %th Name - %th Duration - %th Finished at - - if @ci_project && @ci_project.coverage_enabled? - %th Coverage - %th - = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried, - locals: { coverage: @ci_project.try(:coverage_enabled?), stage: true } += render "builds" diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml index f11a41cfd7b..fcccb002d7e 100644 --- a/app/views/projects/commits/_head.html.haml +++ b/app/views/projects/commits/_head.html.haml @@ -3,6 +3,11 @@ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do Commits %span.badge= number_with_delimiter(@repository.commit_count) + + = nav_link(controller: %w(network)) do + = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do + Network + = nav_link(controller: :compare) do = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do Compare diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 950ab33825e..503d156661e 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,4 +1,4 @@ -.alert_holder += content_for :flash_message do - if current_user && can?(current_user, :download_code, @project) = render 'shared/no_ssh' = render 'shared/no_password' diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 21eee70b424..f2011542ca7 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -17,7 +17,7 @@ .input-group.cross-project-reference %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} = cross_project_reference(@project, @issue) - = clipboard_button(clipboard_target: '#cross-project-reference') + = clipboard_button(clipboard_target: 'span#cross-project-reference') .row %section.col-md-9 diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 1eb71990e55..f9cf4910df3 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -6,23 +6,26 @@ .issue-title %span.issue-title-text = link_to_gfm issue.title, issue_path(issue), class: "row_title" - .pull-right.light + %ul.controls.light - if issue.closed? - %span + %li CLOSED + - if issue.assignee - = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name") + %li + = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name") + - note_count = issue.notes.user.count - if note_count > 0 - - = link_to issue_path(issue) + "#notes" do - = icon('comments') - = note_count + %li + = link_to issue_path(issue) + "#notes" do + = icon('comments') + = note_count - else - - = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do - = icon('comments') - = 0 + %li + = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do + = icon('comments') + = note_count .issue-info #{issue.to_reference} · diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 3d7bd78dce3..d64b19ae91a 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -17,7 +17,7 @@ .input-group.cross-project-reference %span#cross-project-reference.slead.has_tooltip{title: 'Cross-project reference'} = cross_project_reference(@project, @merge_request) - = clipboard_button(clipboard_target: '#cross-project-reference') + = clipboard_button(clipboard_target: 'span#cross-project-reference') .row %section.col-md-9 diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index 1d4c9b66c42..cf9570f7c7e 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,33 +1,41 @@ -- ci_commit = merge_request.ci_commit %li{ class: mr_css_classes(merge_request) } .merge-request-title %span.merge-request-title-text = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title" - .pull-right.light - - if ci_commit - = render_ci_status(ci_commit) + %ul.controls.light - if merge_request.merged? - %span + %li = icon('check') MERGED - elsif merge_request.closed? - %span + %li = icon('ban') CLOSED - - note_count = merge_request.mr_and_commit_notes.user.count + + - if merge_request.ci_commit + %li + = render_ci_status(merge_request.ci_commit) + + - if merge_request.open? && merge_request.broken? + %li + = link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: {container: 'body'} do + = icon('exclamation-triangle') + - if merge_request.assignee - - = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") + %li + = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name") + + - note_count = merge_request.mr_and_commit_notes.user.count - if note_count > 0 - - = link_to merge_request_path(merge_request) + "#notes" do - = icon('comments') - = note_count + %li + = link_to merge_request_path(merge_request) + "#notes" do + = icon('comments') + = note_count - else - - = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do - = icon('comments') - = 0 + %li + = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do + = icon('comments') + = note_count .merge-request-info \##{merge_request.iid} · diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index 156922cea41..4172d5a4e88 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -20,13 +20,18 @@ .mr-compare.merge-request %ul.merge-request-tabs.center-top-menu.no-top.no-bottom %li.commits-tab - = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size %li.diffs-tab.active - = link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes %span.badge= @diffs.size + - if @ci_commit + %li.builds-tab.active + = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do + Builds + %span.badge= @statuses.size .tab-content #commits.commits.tab-pane @@ -42,6 +47,9 @@ .alert.alert-danger %h4 This comparison includes a huge diff. %p To preserve performance the line changes are not shown. + - if @ci_commit + #builds.builds.tab-pane + = render "projects/merge_requests/show/builds" :javascript $('.assign-to-me-link').on('click', function(e){ diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index f5aff0877e7..960d1561e73 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -26,8 +26,7 @@ %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .normal %span Request to merge - %span.label-branch - = source_branch_with_namespace(@merge_request) + %span.label-branch= source_branch_with_namespace(@merge_request) %span into = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do = @merge_request.target_branch @@ -44,17 +43,22 @@ - if @commits.present? %ul.merge-request-tabs.center-top-menu.no-top.no-bottom %li.notes-tab - = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do + = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do Discussion %span.badge= @merge_request.mr_and_commit_notes.user.count %li.commits-tab - = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#commits', action: 'commits', toggle: 'tab'} do + = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do Commits %span.badge= @commits.size %li.diffs-tab - = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do + = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do Changes %span.badge= @merge_request.diffs.size + - if @ci_commit + %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 + %span.badge= @statuses.size .tab-content #notes.notes.tab-pane.voting_notes @@ -63,6 +67,8 @@ - # This tab is always loaded via AJAX #diffs.diffs.tab-pane - # This tab is always loaded via AJAX + #builds.builds.tab-pane + - # This tab is always loaded via AJAX .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml new file mode 100644 index 00000000000..eab5be488b5 --- /dev/null +++ b/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml @@ -0,0 +1,2 @@ +:plain + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/accept'))}"); diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml index 518ecb9f00f..92ce479d463 100644 --- a/app/views/projects/merge_requests/merge.js.haml +++ b/app/views/projects/merge_requests/merge.js.haml @@ -1,6 +1,10 @@ -- if @status +- case @status +- when :success :plain merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'}); +- when :merge_when_build_succeeds + :plain + $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}"); - else :plain $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}"); diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml new file mode 100644 index 00000000000..307a75d02ca --- /dev/null +++ b/app/views/projects/merge_requests/show/_builds.html.haml @@ -0,0 +1 @@ += render "projects/commit/builds", link_to_commit: true diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 49aab961712..b05ab869215 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -1,29 +1,33 @@ - if @ci_commit - - status = @ci_commit.status .mr-widget-heading - .ci_widget{class: "ci-#{status}"} + .ci_widget{class: "ci-#{@ci_commit.status}"} = ci_status_icon(@ci_commit) - %span CI build #{status} - for #{@merge_request.last_commit_short_sha}. + %span + Build + = ci_status_label(@ci_commit) + for + = succeed "." do + = link_to @ci_commit.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @ci_commit.sha), class: "monospace" %span.ci-coverage - = link_to "View build details", ci_status_path(@ci_commit) + = 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 .mr-widget-heading - - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status| + - %w[success skipped canceled failed running pending].each do |status| .ci_widget{class: "ci-#{status}", style: "display:none"} - - if status == :success - - status = "passed" - = icon("check-circle") - - else - = icon("circle") - %span CI build #{status} - for #{@merge_request.last_commit_short_sha}. + = ci_icon_for_status(status) + %span + CI build + = ci_label_for_status(status) + for + - commit = @merge_request.last_commit + = succeed "." do + = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" %span.ci-coverage - - if ci_build_details_path(@merge_request) - = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink" + - if details_path = ci_build_details_path(@merge_request) + = link_to "View details", details_path, :"data-no-turbolink" => "data-no-turbolink" .ci_widget = icon("spinner spin") diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml index 5c6fece8c5c..8c2b5366a06 100644 --- a/app/views/projects/merge_requests/widget/_merged.html.haml +++ b/app/views/projects/merge_requests/widget/_merged.html.haml @@ -14,7 +14,7 @@ = @merge_request.target_branch The source branch has been removed. - - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) + - elsif @merge_request.can_remove_source_branch?(current_user) .remove_source_branch_widget %p = succeed '.' do @@ -50,5 +50,3 @@ $('.remove_source_branch_in_progress').hide(); $('.remove_source_branch_widget.failed').show(); }); - - diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml index 8629129c0b1..55dbae598d3 100644 --- a/app/views/projects/merge_requests/widget/_open.html.haml +++ b/app/views/projects/merge_requests/widget/_open.html.haml @@ -13,6 +13,8 @@ = render 'projects/merge_requests/widget/open/conflicts' - elsif @merge_request.work_in_progress? = render 'projects/merge_requests/widget/open/wip' + - elsif @merge_request.merge_when_build_succeeds? + = render 'projects/merge_requests/widget/open/merge_when_build_succeeds' - elsif !@merge_request.can_be_merged_by?(current_user) = render 'projects/merge_requests/widget/open/not_allowed' - elsif @merge_request.can_be_merged? diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 6d12af16140..c6bc4ca5beb 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -3,26 +3,60 @@ = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f| = hidden_field_tag :authenticity_token, form_authenticity_token .accept-merge-holder.clearfix.js-toggle-container - .accept-action - = f.button class: "btn btn-create accept_merge_request#{status_class}" do - Accept Merge Request - - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork? - .accept-control.checkbox - = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do - = check_box_tag :should_remove_source_branch - Remove source branch - .accept-control.right - = link_to "#", class: "modify-merge-commit-link js-toggle-button" do - = icon('edit') - Modify commit message - .js-toggle-content.hide.prepend-top-20 + .clearfix + .accept-action + - if @ci_commit && @ci_commit.active? + %span.btn-group + = link_to "#", class: "btn btn-create merge_when_build_succeeds" do + Merge When Build Succeeds + %a.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' } + %span.caret + %span.sr-only + Select Merge Moment + %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' } + %li + = link_to "#", class: "merge_when_build_succeeds" do + = icon('check fw') + Merge When Build Succeeds + %li + = link_to "#", class: "accept_merge_request" do + = icon('warning fw') + Merge Immediately + - else + = f.button class: "btn btn-create btn-grouped accept_merge_request #{status_class}" do + Accept Merge Request + - if @merge_request.can_remove_source_branch?(current_user) + .accept-control.checkbox + = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do + = check_box_tag :should_remove_source_branch + Remove source branch + .accept-control.right + = link_to "#", class: "modify-merge-commit-link js-toggle-button" do + = icon('edit') + Modify commit message + .js-toggle-content.hide.prepend-top-default = render 'shared/commit_message_container', params: params, text: @merge_request.merge_commit_message, rows: 14, hint: true + = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" + :javascript - $('.accept-mr-form').on('ajax:before', function() { - var btn = $('.accept_merge_request'); - btn.disable(); - btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress"); + $('.accept_merge_request').on('click', function() { + $(this).html("<i class='fa fa-spinner fa-spin'></i> Merge in progress"); + }); + + $('.accept-mr-form').on('ajax:send', function() { + $(".accept-mr-form :input").disable(); + }); + + $('a.accept_merge_request').on('click', function(e) { + e.preventDefault(); + $(this).closest("form").submit(); + }); + + $('a.merge_when_build_succeeds').on('click', function(e) { + e.preventDefault(); + $("#merge_when_build_succeeds").val("1"); + $(this).closest("form").submit(); }); diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml new file mode 100644 index 00000000000..08af124274b --- /dev/null +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -0,0 +1,26 @@ +%h4 + Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} + to be merged automatically when the build succeeds. +%div + - should_remove_source_branch = @merge_request.merge_params["should_remove_source_branch"].present? + %p + = succeed '.' do + The changes will be merged into + %span.label-branch= @merge_request.target_branch + - if should_remove_source_branch + The source branch will be removed. + - else + The source branch will not be removed. + + - remove_source_branch_button = @merge_request.can_remove_source_branch?(current_user) && !should_remove_source_branch + - user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_build_succeeds?(current_user) + - if remove_source_branch_button || user_can_cancel_automatic_merge + .clearfix.prepend-top-10 + - if remove_source_branch_button + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = icon('times') + Remove Source Branch When Merged + + - if user_can_cancel_automatic_merge + = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do + Cancel Automatic Merge diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml index 9e0e0dc6bb0..28a617538b5 100644 --- a/app/views/projects/network/_head.html.haml +++ b/app/views/projects/network/_head.html.haml @@ -1,4 +1,4 @@ -.gray-content-block.top-block.append-bottom-default +.gray-content-block.append-bottom-default .tree-ref-holder = render partial: 'shared/ref_switcher', locals: {destination: 'graph'} diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index 16005161df6..8065663ca2a 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -1,5 +1,6 @@ - page_title "Network", @ref -= header_title project_title(@project, "Network", namespace_project_network_path(@project.namespace, @project, current_ref)) += render "projects/commits/header_title" += render "projects/commits/head" = render "head" .project-network .controls diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml index d2810f9707a..1c2458fa144 100644 --- a/app/views/projects/project_members/_group_members.html.haml +++ b/app/views/projects/project_members/_group_members.html.haml @@ -10,7 +10,7 @@ = icon('pencil-square-o') Manage group members %ul.content-list - - members.each do |member| + - members.limit(20).each do |member| = render 'groups/group_members/group_member', member: member, show_controls: false - if members.count > 20 %li diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 12356dbcb6b..cefe33e581f 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -30,3 +30,7 @@ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do = icon('folder fw') New directory + - elsif !on_top_of_branch? + %li + %span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch.", data: {container: 'body'}} + = icon('plus') diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 55aa045fd59..111219f2064 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -4,7 +4,7 @@ .form-group.branch = label_tag 'new_branch', 'Target branch', class: 'control-label' .col-sm-10 - = text_field_tag 'new_branch', @new_branch || @ref, required: true, class: "form-control js-new-branch" + = text_field_tag 'new_branch', @new_branch || tree_edit_branch, required: true, class: "form-control js-new-branch" .js-create-merge-request-container .checkbox diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml index f8faa076026..669e6119fb6 100644 --- a/app/views/shared/snippets/_header.html.haml +++ b/app/views/shared/snippets/_header.html.haml @@ -1,6 +1,6 @@ .issuable-details .page-title - .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level), data: { container: 'body' }} + .snippet-box.has_tooltip{class: visibility_level_color(@snippet.visibility_level), title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: 'body' }} = visibility_level_icon(@snippet.visibility_level, fw: false) = visibility_level_label(@snippet.visibility_level) Snippet ##{@snippet.id} diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb index 5d1a8555b7d..c87c0a252b1 100644 --- a/app/workers/merge_worker.rb +++ b/app/workers/merge_worker.rb @@ -8,16 +8,7 @@ class MergeWorker current_user = User.find(current_user_id) merge_request = MergeRequest.find(merge_request_id) - result = MergeRequests::MergeService.new(merge_request.target_project, current_user). - execute(merge_request, params[:commit_message]) - - if result[:status] == :success && params[:should_remove_source_branch].present? - DeleteBranchService.new(merge_request.source_project, current_user). - execute(merge_request.source_branch) - - merge_request.source_project.repository.expire_branch_names - end - - result + MergeRequests::MergeService.new(merge_request.target_project, current_user, params). + execute(merge_request) end end |