diff --git a/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js b/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js
new file mode 100644
index 0000000000000000000000000000000000000000..ae246ada01b3652c237b7f131961d413a896ffa5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js
@@ -0,0 +1,23 @@
+import { statusClassToSvgMap } from '../pipeline_svg_icons';
+
+export default {
+ name: 'PipelineStatusIcon',
+ props: {
+ pipelineStatus: { type: Object, required: true, default: () => ({}) },
+ },
+ computed: {
+ svg() {
+ return statusClassToSvgMap[this.pipelineStatus.icon];
+ },
+ statusClass() {
+ return `ci-status-icon ci-status-icon-${this.pipelineStatus.group}`;
+ },
+ },
+ template: `
+
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/pipeline_svg_icons.js b/app/assets/javascripts/vue_shared/pipeline_svg_icons.js
new file mode 100644
index 0000000000000000000000000000000000000000..5af30ae74f0edc6c1bf17c01a8041198fc7d34ac
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/pipeline_svg_icons.js
@@ -0,0 +1,43 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+import canceledBorderlessSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdBorderlessSvg from 'icons/_icon_status_created_borderless.svg';
+import failedBorderlessSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualBorderlessSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingBorderlessSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningBorderlessSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedBorderlessSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successBorderlessSvg from 'icons/_icon_status_success_borderless.svg';
+import warningBorderlessSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export const statusClassToSvgMap = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+};
+
+export const statusClassToBorderlessSvgMap = {
+ icon_status_canceled: canceledBorderlessSvg,
+ icon_status_created: createdBorderlessSvg,
+ icon_status_failed: failedBorderlessSvg,
+ icon_status_manual: manualBorderlessSvg,
+ icon_status_pending: pendingBorderlessSvg,
+ icon_status_running: runningBorderlessSvg,
+ icon_status_skipped: skippedBorderlessSvg,
+ icon_status_success: successBorderlessSvg,
+ icon_status_warning: warningBorderlessSvg,
+};
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 87667f39ab88c5d3d3c1d076770af14faebd9840..1b7d4e4225813a89dc07272791233cbefbeadebe 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,4 +1,5 @@
-.ci-status-icon-success {
+.ci-status-icon-success,
+.ci-status-icon-passed {
color: $green-500;
svg {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 9507fae301c41d86d0293cd1649e258501b835fd..fe69558d1f2718fe2541f4f2b5948a17a437648e 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -37,12 +37,6 @@
@include btn-red;
}
}
-
- .dropdown-toggle {
- .fa {
- color: inherit;
- }
- }
}
.accept-control {
@@ -89,12 +83,12 @@
}
.ci_widget {
- border-bottom: 1px solid $well-inner-border;
color: $gl-text-color;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
+ padding: $gl-padding-top $gl-padding 0 $gl-padding;
i,
svg {
@@ -115,16 +109,15 @@
flex-wrap: wrap;
}
- .ci-status-icon > .icon-link > svg {
+ .ci-status-icon > .icon-link svg {
width: 22px;
height: 22px;
}
}
.mr-widget-body,
- .ci_widget,
.mr-widget-footer {
- padding: 16px;
+ margin: 16px;
}
.mr-widget-pipeline-graph {
@@ -168,10 +161,23 @@
color: $gl-text-color;
}
+ .capitalize {
+ text-transform: capitalize;
+ }
+
.js-deployment-link {
display: inline-block;
}
+ .mr-widget-help {
+ margin: $gl-padding;
+ color: $ci-skipped-color;
+ }
+
+ .mr-links.mr-info-list {
+ margin: 0 0 $gl-padding 26px;
+ }
+
.mr-widget-body {
h4 {
font-weight: 600;
@@ -189,6 +195,33 @@
margin-right: 7px;
}
+ label {
+ font-weight: normal;
+ }
+
+ .spacing {
+ margin: 0 $gl-padding;
+ }
+
+ .bold {
+ font-weight: bold;
+ color: #5c5c5c;
+ }
+
+ .danger {
+ color: $gl-danger;
+ }
+
+ .mr-widget-help {
+ margin: $gl-padding 0;
+ }
+
+ .dropdown-toggle {
+ .fa {
+ color: inherit;
+ }
+ }
+
@media (max-width: $screen-xs-max) {
h4 {
font-size: 14px;
@@ -220,6 +253,12 @@
margin: 0;
}
}
+
+ .commit-message-editor {
+ label {
+ padding: 0;
+ }
+ }
}
.mr-widget-footer {
@@ -345,61 +384,50 @@
}
}
-.remove-message-pipes {
- ul {
- margin: 10px 0 0 12px;
- padding: 0;
- list-style: none;
- border-left: 2px solid $border-color;
- display: inline-block;
- }
+.mr-info-list {
+ position: relative;
+ overflow: hidden;
+ margin: 10px 0 $gl-padding 12px;
- li {
+ p {
+ margin: 6px 0;
position: relative;
- margin: 0;
- padding: 0;
- display: block;
-
- span {
- margin-left: 15px;
- max-height: 20px;
- }
- }
-
- li::before {
- content: '';
- position: absolute;
- border-top: 2px solid $border-color;
- height: 1px;
- top: 8px;
- width: 8px;
- }
+ padding-left: 15px;
- li:last-child {
&::before {
- top: 18px;
+ content: '';
+ position: absolute;
+ border-top: 2px solid $border-color;
+ height: 1px;
+ top: 8px;
+ width: 8px;
+ left: 0px;
}
- span {
- display: block;
- position: relative;
- top: 5px;
- margin-top: 5px;
+ &:last-child {
+ margin-bottom: 0;
+ &::before {
+ top: 14px;
+ }
}
}
+
+ .legend {
+ height: 100%;
+ width: 2px;
+ background: $border-color;
+ position: absolute;
+ top: -5px;
+ }
}
.mr-source-target {
background-color: $gray-light;
- line-height: 31px;
- border-style: solid;
- border-width: 1px;
- border-color: $border-color;
- border-top-right-radius: 3px;
- border-top-left-radius: 3px;
- border-bottom: none;
- padding: 16px;
- margin-bottom: -1px;
+ border-radius: 3px 3px 0 0;
+ border-bottom: 1px solid $border-color;
+ padding: 0 $gl-padding;
+ margin-bottom: 6px;
+ line-height: 44px;
}
.panel-new-merge-request {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 31eca8f3442de858c9f12e2a420afcaa7b819938..589ad4649ee16a659166e98ff36f1d6662fa0eaa 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -102,7 +102,10 @@ def can?(object, action, subject = :global)
end
def access_denied!
- render "errors/access_denied", layout: "errors", status: 404
+ respond_to do |format|
+ format.json { head :unauthorized }
+ format.any { render "errors/access_denied", layout: "errors", status: 404 }
+ end
end
def git_not_found!
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 840405f38cbbbc5631edbf6f993167bb626232f7..d78b71c783fdb77eebc24b4d516a79371cc7d99f 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -71,7 +71,9 @@ def destroy
redirect_to namespace_project_branches_path(@project.namespace,
@project), status: 303
end
+ # TODO: @oswaldo - Handle only JSON and HTML after deleting existing MR widget.
format.js { render nothing: true, status: status[:return_code] }
+ format.json { render json: { message: status[:message] }, status: status[:return_code] }
end
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index cc67f688d51ddb20c9d0ab2bbe72e5dea4bef5eb..a6d442004ea337356a4c2fefc76ac91f17c6c5f8 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -36,7 +36,7 @@ def pipelines
format.html
format.json do
render json: PipelineSerializer
- .new(project: @project, user: @current_user)
+ .new(project: @project, current_user: @current_user)
.represent(@pipelines)
end
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 8a4ef4d213ea2b09cb31a8cd70cffb9d125125ee..b08849d0ca845e159ea10e39bd3d182739d03182 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -81,11 +81,13 @@ def stop
stop_action = @environment.stop_with_action!(current_user)
- if stop_action
- redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
+ action_or_env_url = if stop_action
+ polymorphic_url([project.namespace.becomes(Namespace), project, stop_action])
else
- redirect_to namespace_project_environment_path(project.namespace, project, @environment)
+ namespace_project_environment_url(project.namespace, project, @environment)
end
+
+ render json: { redirect_url: action_or_env_url }
end
def terminal
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index aa40a2bfb1858b0c15a546f0f786c3de7f98820e..e07fa3443b00bd42082b9df0728f6bef51f7c5ea 100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -77,10 +77,12 @@ def index
def show
respond_to do |format|
- format.html { define_discussion_vars }
+ format.html do
+ define_discussion_vars
+ end
format.json do
- render json: MergeRequestSerializer.new.represent(@merge_request, type: :full)
+ render json: @merge_request_json
end
format.patch do
@@ -236,7 +238,7 @@ def pipelines
format.json do
render json: PipelineSerializer
- .new(project: @project, user: @current_user)
+ .new(project: @project, current_user: @current_user)
.represent(@pipelines)
end
end
@@ -250,7 +252,7 @@ def new
render json: {
pipelines: PipelineSerializer
- .new(project: @project, user: @current_user)
+ .new(project: @project, current_user: @current_user)
.represent(@pipelines)
}
end
@@ -327,17 +329,37 @@ def update
end
def remove_wip
- MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request)
+ @merge_request = MergeRequests::UpdateService
+ .new(project, current_user, wip_event: 'unwip')
+ .execute(@merge_request)
+
+ # TODO: @oswaldo - Handle only JSON after deleting existing MR widget.
+ respond_to do |format|
+ format.json do
+ render json: serializer.represent(@merge_request).to_json
+ end
- redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
- notice: "The merge request can now be merged."
+ format.html do
+ redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
+ notice: "The merge request can now be merged."
+
+ end
+ end
end
def merge_check
@merge_request.check_if_can_be_merged
@pipelines = @merge_request.all_pipelines
- render partial: "projects/merge_requests/widget/show.html.haml", layout: false
+ respond_to do |format|
+ format.js do
+ render partial: "projects/merge_requests/widget/show.html.haml", layout: false
+ end
+
+ format.json do
+ render json: serializer.represent(@merge_request).to_json
+ end
+ end
end
def cancel_merge_when_pipeline_succeeds
@@ -348,56 +370,63 @@ def cancel_merge_when_pipeline_succeeds
MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user)
.cancel(@merge_request)
+
+ # TODO: @oswaldo - Handle only JSON after deleting existing MR widget.
+ respond_to do |format|
+ format.json do
+ render json: serializer.represent(@merge_request.reload).to_json
+ end
+
+ format.js
+ end
end
def merge
return access_denied! unless @merge_request.can_be_merged_by?(current_user)
return render_404 unless @merge_request.approved?
+ @status = merge!
+
+ # TODO: @oswaldo - Handle only JSON after deleting existing MR widget.
+ respond_to do |format|
+ format.json { render json: { status: @status } }
+ format.js
+ end
+ end
+
+ def merge!
# Disable the CI check if merge_when_pipeline_succeeds is enabled since we have
# to wait until CI completes to know
unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?)
- @status = :failed
- return
+ return :failed
end
- merge_request_service = MergeRequests::MergeService.new(@project, current_user, merge_params)
-
- unless merge_request_service.hooks_validation_pass?(@merge_request)
- @status = :hook_validation_error
- return
- end
-
- if params[:sha] != @merge_request.diff_head_sha
- @status = :sha_mismatch
- return
- end
+ return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
@merge_request.update(merge_error: nil, squash: merge_params[:squash])
if params[:merge_when_pipeline_succeeds].present?
- unless @merge_request.head_pipeline
- @status = :failed
- return
- end
+ return :failed unless @merge_request.head_pipeline
if @merge_request.head_pipeline.active?
MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user, merge_params)
.execute(@merge_request)
- @status = :merge_when_pipeline_succeeds
+ :merge_when_pipeline_succeeds
elsif @merge_request.head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
- @status = :success
+
+ :success
else
- @status = :failed
+ :failed
end
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
- @status = :success
+
+ :success
end
end
@@ -471,6 +500,7 @@ def assign_related_issues
end
end
+ # TODO: @oswaldo - remove it when deleting old widget parts
def ci_status
pipeline = @merge_request.head_pipeline
@pipelines = @merge_request.all_pipelines
@@ -492,8 +522,11 @@ def ci_status
sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status,
coverage: coverage,
+ has_ci: @merge_request.has_ci?,
pipeline: pipeline.try(:id),
- has_ci: @merge_request.has_ci?
+ stages: PipelineSerializer
+ .new(project: @project, current_user: @current_user)
+ .represent_stages(pipeline)
}
render json: response
@@ -501,7 +534,7 @@ def ci_status
def pipeline_status
render json: PipelineSerializer
- .new(project: @project, user: @current_user)
+ .new(project: @project, current_user: @current_user)
.represent_status(@merge_request.head_pipeline)
end
@@ -621,6 +654,8 @@ def define_show_vars
labels
define_pipelines_vars
+
+ @merge_request_json = serializer.represent(@merge_request).to_json
end
# Discussion tab data is rendered on html responses of actions
@@ -803,4 +838,12 @@ def close_merge_request_without_source_project
@merge_request.close
end
end
+
+ def serializer
+ if params[:basic]
+ MergeRequestBasicSerializer.new
+ else
+ MergeRequestSerializer.new(current_user: current_user, project: merge_request.project)
+ end
+ end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 43a1abaa662f757cd949050035b182b47151dba7..7b853a5d874ec16eac7717b95766809abeef17fd 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -31,7 +31,7 @@ def index
format.json do
render json: {
pipelines: PipelineSerializer
- .new(project: @project, user: @current_user)
+ .new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(@pipelines),
count: {
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 762ff85d9ceeb5000745aac90ccdba1d9b3adfff..e5600953b96393fced33d1d5c3485f8d16996627 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -37,7 +37,7 @@ def serialize_issuable(issuable)
when Issue
IssueSerializer.new.represent(issuable).to_json
when MergeRequest
- MergeRequestSerializer.new.represent(issuable).to_json
+ @merge_request_json
end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index f14438ef598ea6e2190346ea85b6a8e2ca3e17dd..9a905d81b5aa203209f02189ab2c4279b6b6b2a1 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -55,6 +55,7 @@ def merge_path_description(merge_request, separator)
end
end
+ # TODO: @oswaldo - Delete when removing old widget parts
def issues_sentence(issues)
# Sorting based on the `#123` or `group/project#123` reference will sort
# local issues first.
@@ -63,10 +64,12 @@ def issues_sentence(issues)
end.sort.to_sentence
end
+ # TODO: @oswaldo - Delete when removing old widget parts
def mr_closes_issues
@mr_closes_issues ||= @merge_request.closes_issues(current_user)
end
+ # TODO: @oswaldo - Delete when removing old widget parts
def mr_issues_mentioned_but_not_closing
@mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index a9eedf6edf2f16633baf49b1475d11f55889b54a..0e2a06b871cc3b14c5fe94791c0bcc66520e7838 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -466,7 +466,8 @@ def can_cancel_merge_when_pipeline_succeeds?(current_user)
end
def can_remove_source_branch?(current_user)
- !source_project.protected_branch?(source_branch) &&
+ source_project &&
+ !source_project.protected_branch?(source_branch) &&
!source_project.root_ref?(source_branch) &&
Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head
@@ -958,6 +959,8 @@ def conflicts
end
def conflicts_can_be_resolved_by?(user)
+ return false unless source_project
+
access = ::Gitlab::UserAccess.new(user, project: source_project)
access.can_push_to_branch?(source_branch)
end
diff --git a/app/serializers/event_entity.rb b/app/serializers/event_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..935d67a4f37fb6fc86e9c8336f0e81f950fc5c0a
--- /dev/null
+++ b/app/serializers/event_entity.rb
@@ -0,0 +1,4 @@
+class EventEntity < Grape::Entity
+ expose :author, using: UserEntity
+ expose :updated_at
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..146081b14ea0e573b822317cf4d42d8359f1da31
--- /dev/null
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -0,0 +1,5 @@
+class MergeRequestBasicEntity < Grape::Entity
+ expose :merge_status
+ expose :state
+ expose :source_branch_exists?, as: :source_branch_exists
+end
diff --git a/app/serializers/merge_request_basic_serializer.rb b/app/serializers/merge_request_basic_serializer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ad880270d500274a10ab0d7eeabed6bba7e8781f
--- /dev/null
+++ b/app/serializers/merge_request_basic_serializer.rb
@@ -0,0 +1,4 @@
+class MergeRequestBasicSerializer < BaseSerializer
+ entity MergeRequestBasicEntity
+end
+
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 59c94c613f6c097f0e8a7a55cc74c0c719e21121..8fe3c19b8c5b3bdab0467378b15a4a7a480c8cbf 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -1,5 +1,7 @@
class MergeRequestEntity < IssuableEntity
- expose :approvals_before_merge
+ include RequestAwareEntity
+ include GitlabMarkdownHelper
+
expose :in_progress_merge_commit_sha
expose :locked_at
expose :merge_commit_sha
@@ -14,4 +16,224 @@ class MergeRequestEntity < IssuableEntity
expose :source_project_id
expose :target_branch
expose :target_project_id
+
+ # EE specific
+ expose :approvals_before_merge
+
+ # Events
+ expose :merge_event, using: EventEntity
+ expose :closed_event, using: EventEntity
+
+ # User entities
+ expose :author, using: UserEntity
+ expose :merge_user, using: UserEntity
+
+ # Diff sha's
+ expose :diff_head_sha
+ expose :diff_head_commit_short_id do |merge_request|
+ merge_request.diff_head_commit.try(:short_id)
+ end
+
+ expose :merge_commit_sha
+ expose :merge_commit_message
+ expose :head_pipeline, with: PipelineEntity, as: :pipeline
+
+ # Booleans
+ expose :work_in_progress?, as: :work_in_progress
+ expose :source_branch_exists?, as: :source_branch_exists
+ expose :mergeable_discussions_state?, as: :mergeable_discussions_state
+ expose :conflicts_can_be_resolved_in_ui?, as: :conflicts_can_be_resolved_in_ui
+ expose :branch_missing?, as: :branch_missing
+ expose :has_no_commits?, as: :has_no_commits
+ expose :can_be_cherry_picked?, as: :can_be_cherry_picked
+ expose :cannot_be_merged?, as: :has_conflicts
+ expose :can_be_merged?, as: :can_be_merged
+
+ # CI related
+ expose :has_ci?, as: :has_ci
+ expose :ci_status do |merge_request|
+ pipeline = merge_request.head_pipeline
+
+ if pipeline
+ status = pipeline.status
+ status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings?
+
+ status || "preparing"
+ else
+ ci_service = merge_request.source_project.try(:ci_service)
+ ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
+ end
+ end
+
+ expose :issues_links do
+ expose :closing do |merge_request|
+ closes_issues = merge_request.closes_issues(current_user)
+
+ markdown issues_sentence(merge_request.project, closes_issues),
+ pipeline: :gfm,
+ author: merge_request.author,
+ project: merge_request.project
+ end
+
+ expose :mentioned_but_not_closing do |merge_request|
+ mentioned_but_not_closing_issues = merge_request
+ .issues_mentioned_but_not_closing(current_user)
+
+ markdown issues_sentence(merge_request.project, mentioned_but_not_closing_issues),
+ pipeline: :gfm,
+ author: merge_request.author,
+ project: merge_request.project
+ end
+ end
+
+ expose :current_user do
+ expose :can_create_issue do |merge_request|
+ merge_request.project.issues_enabled? &&
+ can?(request.current_user, :create_issue, merge_request.project)
+ end
+
+ expose :can_update_merge_request do |merge_request|
+ merge_request.project.merge_requests_enabled? &&
+ can?(request.current_user, :update_merge_request, merge_request.project)
+ end
+
+ expose :can_resolve_conflicts do |merge_request|
+ merge_request.conflicts_can_be_resolved_by?(request.current_user)
+ end
+
+ expose :can_remove_source_branch do |merge_request|
+ merge_request.can_remove_source_branch?(request.current_user)
+ end
+
+ expose :can_merge do |merge_request|
+ merge_request.can_be_merged_by?(request.current_user)
+ end
+
+ expose :can_merge_via_cli do |merge_request|
+ merge_request.can_be_merged_via_command_line_by?(request.current_user)
+ end
+
+ expose :can_revert do |merge_request|
+ merge_request.can_be_reverted?(request.current_user)
+ end
+
+ expose :can_cancel_automatic_merge do |merge_request|
+ merge_request.can_cancel_merge_when_pipeline_succeeds?(request.current_user)
+ end
+ end
+
+ expose :target_branch_path do |merge_request|
+ namespace_project_branch_path(merge_request.target_project.namespace,
+ merge_request.target_project,
+ merge_request.target_branch)
+ end
+
+ expose :source_branch_path do |merge_request|
+ namespace_project_branch_path(merge_request.source_project.namespace,
+ merge_request.source_project,
+ merge_request.source_branch)
+ end
+
+ expose :project_archived do |merge_request|
+ merge_request.project.archived?
+ end
+
+ expose :conflict_resolution_ui_path do |merge_request|
+ conflicts_namespace_project_merge_request_path(merge_request.project.namespace,
+ merge_request.project,
+ merge_request)
+ end
+
+ expose :remove_wip_path do |merge_request|
+ remove_wip_namespace_project_merge_request_path(merge_request.project.namespace,
+ merge_request.project,
+ merge_request)
+ end
+
+ expose :merge_path do |merge_request|
+ merge_namespace_project_merge_request_path(merge_request.project.namespace,
+ merge_request.project,
+ merge_request)
+ end
+
+ expose :cancel_merge_when_pipeline_succeeds_path do |merge_request|
+ cancel_merge_when_pipeline_succeeds_namespace_project_merge_request_path(
+ merge_request.target_project.namespace,
+ merge_request.target_project,
+ merge_request)
+ end
+
+ expose :merge_commit_message_with_description do |merge_request|
+ merge_request.merge_commit_message(include_description: true)
+ end
+
+ expose :diverged_commits_count do |merge_request|
+ merge_request.open? &&
+ merge_request.diverged_from_target_branch? ?
+ merge_request.diverged_commits_count : 0
+ end
+
+ expose :email_patches_path do |merge_request|
+ namespace_project_merge_request_path(merge_request.target_project.namespace,
+ merge_request.target_project,
+ merge_request,
+ format: :patch)
+ end
+
+ expose :plain_diff_path do |merge_request|
+ namespace_project_merge_request_path(merge_request.target_project.namespace,
+ merge_request.target_project,
+ merge_request,
+ format: :diff)
+ end
+
+ expose :ci_status_path do |merge_request|
+ ci_status_namespace_project_merge_request_path(merge_request.target_project.namespace,
+ merge_request.target_project,
+ merge_request)
+ end
+
+ # FIXME: @oswaldo, please implement this
+ expose :status_path do |merge_request|
+ path = namespace_project_merge_request_path(merge_request.target_project.namespace,
+ merge_request.target_project,
+ merge_request,
+ format: :diff)
+ path.sub! 'diff', 'json'
+ end
+
+ # TODO: @oswaldo, please verify this
+ expose :merge_check_path do |merge_request|
+ merge_check_namespace_project_merge_request_path(merge_request.target_project.namespace,
+ merge_request.target_project,
+ merge_request)
+ end
+
+ expose :only_allow_merge_if_pipeline_succeeds do |merge_request|
+ merge_request.project.only_allow_merge_if_pipeline_succeeds?
+ end
+
+ expose :create_issue_to_resolve_discussions_path do |merge_request|
+ new_namespace_project_issue_path(merge_request.project.namespace,
+ merge_request.project,
+ merge_request_for_resolving_discussions_of: merge_request.iid)
+ end
+
+ expose :ci_environments_status_url do |merge_request|
+ ci_environments_status_namespace_project_merge_request_path(merge_request.project.namespace,
+ merge_request.project,
+ merge_request)
+ end
+
+ private
+
+ delegate :current_user, to: :request
+
+ def issues_sentence(project, issues)
+ # Sorting based on the `#123` or `group/project#123` reference will sort
+ # local issues first.
+ issues.map do |issue|
+ issue.to_reference(project)
+ end.sort.to_sentence
+ end
end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 3f16dd66d54b4c6f235ed64d0ad85e304823a604..27d37466d82ea43e7e7180539551e01f3278b9fb 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -3,6 +3,8 @@ class PipelineEntity < Grape::Entity
expose :id
expose :user, using: UserEntity
+ expose :active?, as: :active
+ expose :coverage
expose :path do |pipeline|
namespace_project_pipeline_path(
@@ -70,15 +72,15 @@ class PipelineEntity < Grape::Entity
def can_retry?
pipeline.retryable? &&
- can?(request.user, :update_pipeline, pipeline)
+ can?(request.current_user, :update_pipeline, pipeline)
end
def can_cancel?
pipeline.cancelable? &&
- can?(request.user, :update_pipeline, pipeline)
+ can?(request.current_user, :update_pipeline, pipeline)
end
def detailed_status
- pipeline.detailed_status(request.user)
+ pipeline.detailed_status(request.current_user)
end
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index 7829df9fada1a623bb62addefeea2bd6121af80e..0895dd7382224cc2c64c48d15e1ff889bcc18fcb 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -29,4 +29,11 @@ def represent_status(resource)
data = represent(resource, { only: [{ details: [:status] }] })
data.dig(:details, :status) || {}
end
+
+ def represent_stages(resource)
+ return {} unless resource.present?
+
+ data = represent(resource, { only: [{ details: [:stages] }] })
+ data.dig(:details, :stages) || []
+ end
end
diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb
index 7a047bdc712388a5d6f96899d467ecad13c84d04..157ab1776a70d6441b89d5e39735eb2f7c22d7a5 100644
--- a/app/serializers/stage_entity.rb
+++ b/app/serializers/stage_entity.rb
@@ -33,6 +33,6 @@ class StageEntity < Grape::Entity
alias_method :stage, :object
def detailed_status
- stage.detailed_status(request.user)
+ stage.detailed_status(request.current_user)
end
end
diff --git a/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
index eab5be488b5d32faed0037aa1b9004e5901dc88f..1f803f4001a87a12fecd87332eec0d1f0b64163f 100644
--- a/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
+++ b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
@@ -1,2 +1,2 @@
:plain
- $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/accept'))}");
+ $('.mr-widget-body:eq(0)').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 84c90ddf9552fe62e0cb5b03d8bdd013a0e70a77..27fb3ae0dd5a1590302a9b1ae7b2c6a12143f862 100644
--- a/app/views/projects/merge_requests/merge.js.haml
+++ b/app/views/projects/merge_requests/merge.js.haml
@@ -11,7 +11,7 @@
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/error'))}");
- when :sha_mismatch
:plain
- $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}");
+ $('.mr-widget-body:eq(0)').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}");
- else
:plain
- $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}");
+ $('.mr-widget-body:eq(0)').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}");
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 0b0fb7854c2171316aef143a1fc1ffd41349f80f..ce6759945b839d87478b83745d7af6fc0c19a35b 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -7,6 +7,9 @@
- elsif @merge_request.locked?
= render 'projects/merge_requests/widget/locked'
+:javascript
+ window.gl.mrWidgetData = #{@merge_request_json}
+
:javascript
var opts = {
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
@@ -37,3 +40,8 @@
}
merge_request_widget = new window.gl.MergeRequestWidget(opts);
+
+#js-vue-mr-widget.mr-widget
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('vue_merge_request_widget')
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 09c62bbe25b131f81ba683454389dffc19f86bda..d5b1f8d8ffec0fd058429ee1cb6902fa60342c25 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -47,6 +47,7 @@ var config = {
u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
vue_pipelines: './vue_pipelines_index/index.js',
+ vue_merge_request_widget: './vue_merge_request_widget/index.js',
},
output: {
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index d20e7368086aadec04067a7279626ff919e8af73..23f55f05d1555ccb011526980bd90aa2596eca5f 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -177,33 +177,98 @@
sign_in(user)
post :destroy,
- format: :js,
- id: branch,
- namespace_id: project.namespace,
- project_id: project
+ format: format,
+ id: branch,
+ namespace_id: project.namespace,
+ project_id: project
end
- context "valid branch name, valid source" do
+ context 'as JS' do
let(:branch) { "feature" }
+ let(:format) { :js }
- it { expect(response).to have_http_status(200) }
- end
+ context "valid branch name, valid source" do
+ let(:branch) { "feature" }
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.body).to be_blank }
+ end
+
+ context "valid branch name with unencoded slashes" do
+ let(:branch) { "improve/awesome" }
+
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.body).to be_blank }
+ end
+
+ context "valid branch name with encoded slashes" do
+ let(:branch) { "improve%2Fawesome" }
- context "valid branch name with unencoded slashes" do
- let(:branch) { "improve/awesome" }
+ it { expect(response).to have_http_status(200) }
+ it { expect(response.body).to be_blank }
+ end
- it { expect(response).to have_http_status(200) }
+ context "invalid branch name, valid ref" do
+ let(:branch) { "no-branch" }
+
+ it { expect(response).to have_http_status(404) }
+ it { expect(response.body).to be_blank }
+ end
end
- context "valid branch name with encoded slashes" do
- let(:branch) { "improve%2Fawesome" }
+ context 'as JSON' do
+ let(:branch) { "feature" }
+ let(:format) { :json }
+
+ context 'valid branch name, valid source' do
+ let(:branch) { "feature" }
- it { expect(response).to have_http_status(200) }
+ it 'returns JSON response with message' do
+ expect(json_response).to eql("message" => 'Branch was removed')
+ end
+
+ it { expect(response).to have_http_status(200) }
+ end
+
+ context 'valid branch name with unencoded slashes' do
+ let(:branch) { "improve/awesome" }
+
+ it 'returns JSON response with message' do
+ expect(json_response).to eql('message' => 'Branch was removed')
+ end
+
+ it { expect(response).to have_http_status(200) }
+ end
+
+ context "valid branch name with encoded slashes" do
+ let(:branch) { 'improve%2Fawesome' }
+
+ it 'returns JSON response with message' do
+ expect(json_response).to eql('message' => 'Branch was removed')
+ end
+
+ it { expect(response).to have_http_status(200) }
+ end
+
+ context 'invalid branch name, valid ref' do
+ let(:branch) { 'no-branch' }
+
+ it 'returns JSON response with message' do
+ expect(json_response).to eql('message' => 'No such branch')
+ end
+
+ it { expect(response).to have_http_status(404) }
+ end
end
- context "invalid branch name, valid ref" do
- let(:branch) { "no-branch" }
- it { expect(response).to have_http_status(404) }
+ context 'as HTML' do
+ let(:branch) { "feature" }
+ let(:format) { :html }
+
+ it 'redirects to branches path' do
+ expect(response)
+ .to redirect_to(namespace_project_branches_path(project.namespace, project))
+ end
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 2c3cc45f2008ff0172d3f047a212b3cc2790f538..05341c86dd7df1f092532c595ef483b0d17cfe6d 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -151,6 +151,48 @@
end
end
+ describe 'PATCH #stop' do
+ context 'when env not available' do
+ it 'returns 404' do
+ allow_any_instance_of(Environment).to receive(:available?) { false }
+
+ patch :stop, environment_params, format: :json
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when stop action' do
+ it 'returns action url' do
+ action = create(:ci_build, :manual)
+
+ allow_any_instance_of(Environment)
+ .to receive_messages(available?: true, stop_with_action!: action)
+
+ patch :stop, environment_params, format: :json
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq(
+ { 'redirect_url' =>
+ "http://test.host/#{project.path_with_namespace}/builds/#{action.id}" })
+ end
+ end
+
+ context 'when no stop action' do
+ it 'returns env url' do
+ allow_any_instance_of(Environment)
+ .to receive_messages(available?: true, stop_with_action!: nil)
+
+ patch :stop, environment_params, format: :json
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq(
+ { 'redirect_url' =>
+ "http://test.host/#{project.path_with_namespace}/environments/#{environment.id}" })
+ end
+ end
+ end
+
describe 'GET #terminal' do
context 'with valid id' do
it 'responds with a status code 200' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 9f615a2a70a2cb708080fa20e69859ce27df0cae..7724b277f3558e5547a29892e4219cafbb1cd6e3 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -236,63 +236,51 @@ def json_response
end
describe "GET show" do
- shared_examples "export merge as" do |format|
- it "does generally work" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: format)
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid
+ }
- expect(response).to be_success
- end
+ get :show, params.merge(extra_params)
+ end
- it_behaves_like "loads labels", :show
+ it_behaves_like "loads labels", :show
- it "generates it" do
- expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
+ describe 'as html' do
+ it "renders merge request page" do
+ go(format: :html)
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: format)
+ expect(response).to be_success
end
+ end
- it "renders it" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: format)
+ describe 'as json' do
+ context 'with basic param' do
+ it 'renders basic MR entity as json' do
+ go(basic: true, format: :json)
- expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
+ expect(json_response)
+ .to eql(MergeRequestBasicSerializer.new.represent(merge_request).as_json)
+ end
end
- it "does not escape Html" do
- allow_any_instance_of(MergeRequest).to receive(:"to_#{format}").
- and_return('HTML entities &<>" ')
-
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: format)
+ context 'without basic param' do
+ it 'renders the merge request in the json format' do
+ go(format: :json)
- expect(response.body).not_to include('&')
- expect(response.body).not_to include('>')
- expect(response.body).not_to include('<')
- expect(response.body).not_to include('"')
+ expect(json_response).to eql(
+ MergeRequestSerializer
+ .new(current_user: user, project: project)
+ .represent(merge_request).as_json)
+ end
end
end
describe "as diff" do
it "triggers workhorse to serve the request" do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: :diff)
+ go(format: :diff)
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
end
@@ -300,11 +288,7 @@ def json_response
describe "as patch" do
it 'triggers workhorse to serve the request' do
- get(:show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: merge_request.iid,
- format: :patch)
+ go(format: :patch)
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
end
@@ -556,39 +540,166 @@ def update_merge_request(params = {})
project_id: project,
id: merge_request.iid,
squash: false,
- format: 'raw'
+ format: format
}
end
- context 'when the user does not have access' do
- before do
- project.team.truncate
- project.team << [user, :reporter]
- post :merge, base_params
- end
+ context 'as JSON' do
+ let(:format) { 'json' }
+
+ context 'when the user does not have access' do
+ before do
+ project.team.truncate
+ project.team << [user, :reporter]
+ xhr :post, :merge, base_params
+ end
- it 'returns not found' do
- expect(response).to be_not_found
+ it 'returns access denied' do
+ expect(response).to have_http_status(401)
+ end
end
- end
- context 'when the merge request is not mergeable' do
- before do
- merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+ context 'when the merge request is not mergeable' do
+ before do
+ merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+
+ post :merge, base_params
+ end
- post :merge, base_params
+ it 'returns :failed' do
+ expect(json_response).to eq('status' => 'failed')
+ end
end
- it 'returns :failed' do
- expect(assigns(:status)).to eq(:failed)
+ context 'when the sha parameter does not match the source SHA' do
+ before { post :merge, base_params.merge(sha: 'foo') }
+
+ it 'returns :sha_mismatch' do
+ expect(json_response).to eq('status' => 'sha_mismatch')
+ end
end
- end
- context 'when the sha parameter does not match the source SHA' do
- before { post :merge, base_params.merge(sha: 'foo') }
+ context 'when the sha parameter matches the source SHA' do
+ def merge_with_sha
+ post :merge, base_params.merge(sha: merge_request.diff_head_sha)
+ end
+
+ it 'returns :success' do
+ merge_with_sha
+
+ expect(json_response).to eq('status' => 'success')
+ end
+
+ it 'starts the merge immediately' do
+ expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
+
+ merge_with_sha
+ end
+
+ context 'when the pipeline succeeds is passed' do
+ def merge_when_pipeline_succeeds
+ post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
+ end
+
+ before do
+ create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
+ end
+
+ it 'returns :merge_when_pipeline_succeeds' do
+ merge_when_pipeline_succeeds
+
+ expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
+ end
+
+ it 'sets the MR to merge when the pipeline succeeds' do
+ service = double(:merge_when_pipeline_succeeds_service)
+
+ expect(MergeRequests::MergeWhenPipelineSucceedsService)
+ .to receive(:new).with(project, anything, anything)
+ .and_return(service)
+ expect(service).to receive(:execute).with(merge_request)
+
+ merge_when_pipeline_succeeds
+ end
+
+ context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do
+ before do
+ project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
+ end
+
+ it 'returns :merge_when_pipeline_succeeds' do
+ merge_when_pipeline_succeeds
+
+ expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
+ end
+ end
+ end
+
+ describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do
+ let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
+
+ context 'when enabled' do
+ before do
+ project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
+ end
+
+ context 'with unresolved discussion' do
+ before do
+ expect(merge_request).not_to be_discussions_resolved
+ end
+
+ it 'returns :failed' do
+ merge_with_sha
+
+ expect(json_response).to eq('status' => 'failed')
+ end
+ end
+
+ context 'with all discussions resolved' do
+ before do
+ merge_request.discussions.each { |d| d.resolve!(user) }
+ expect(merge_request).to be_discussions_resolved
+ end
+
+ it 'returns :success' do
+ merge_with_sha
+
+ expect(json_response).to eq('status' => 'success')
+ end
+ end
+ end
+
+ context 'when disabled' do
+ before do
+ project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
+ end
+
+ context 'with unresolved discussion' do
+ before do
+ expect(merge_request).not_to be_discussions_resolved
+ end
+
+ it 'returns :success' do
+ merge_with_sha
+
+ expect(json_response).to eq('status' => 'success')
+ end
+ end
+
+ context 'with all discussions resolved' do
+ before do
+ merge_request.discussions.each { |d| d.resolve!(user) }
+ expect(merge_request).to be_discussions_resolved
+ end
+
+ it 'returns :success' do
+ merge_with_sha
- it 'returns :sha_mismatch' do
- expect(assigns(:status)).to eq(:sha_mismatch)
+ expect(json_response).to eq('status' => 'success')
+ end
+ end
+ end
+ end
end
end
@@ -615,47 +726,84 @@ def merge_with_sha(params = {})
end
end
- it 'returns :success' do
- merge_with_sha
+ context 'as any other format' do
+ let(:format) { 'js' }
- expect(assigns(:status)).to eq(:success)
- end
+ context 'when squash is passed as 1' do
+ it 'updates the squash attribute on the MR to true' do
+ merge_request.update(squash: false)
+ merge_with_sha(squash: '1')
+
+ expect(merge_request.reload.squash).to be_truthy
+ end
+ end
- it 'starts the merge immediately' do
- expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
+ context 'when squash is passed as 1' do
+ it 'updates the squash attribute on the MR to false' do
+ merge_request.update(squash: true)
+ merge_with_sha(squash: '0')
- merge_with_sha
+ expect(merge_request.reload.squash).to be_falsey
+ end
+ end
end
- context 'when the pipeline succeeds is passed' do
- def merge_when_pipeline_succeeds
- post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
+ context 'when the user does not have access' do
+ before do
+ project.team.truncate
+ project.team << [user, :reporter]
+ post :merge, base_params
+ end
+
+ it 'returns not found' do
+ expect(response).to be_not_found
end
+ end
+ context 'when the merge request is not mergeable' do
before do
- create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
+ merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+
+ post :merge, base_params
end
- it 'returns :merge_when_pipeline_succeeds' do
- merge_when_pipeline_succeeds
+ it 'returns :failed' do
+ expect(assigns(:status)).to eq(:failed)
+ end
+ end
- expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
+ context 'when the sha parameter does not match the source SHA' do
+ before { post :merge, base_params.merge(sha: 'foo') }
+
+ it 'returns :sha_mismatch' do
+ expect(assigns(:status)).to eq(:sha_mismatch)
+ end
+ end
+
+ context 'when the sha parameter matches the source SHA' do
+ def merge_with_sha
+ post :merge, base_params.merge(sha: merge_request.diff_head_sha)
end
- it 'sets the MR to merge when the pipeline succeeds' do
- service = double(:merge_when_pipeline_succeeds_service)
+ it 'returns :success' do
+ merge_with_sha
+
+ expect(assigns(:status)).to eq(:success)
+ end
- expect(MergeRequests::MergeWhenPipelineSucceedsService)
- .to receive(:new).with(project, anything, anything)
- .and_return(service)
- expect(service).to receive(:execute).with(merge_request)
+ it 'starts the merge immediately' do
+ expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
- merge_when_pipeline_succeeds
+ merge_with_sha
end
- context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do
+ context 'when the pipeline succeeds is passed' do
+ def merge_when_pipeline_succeeds
+ post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
+ end
+
before do
- project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
+ create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
end
it 'returns :merge_when_pipeline_succeeds' do
@@ -663,70 +811,93 @@ def merge_when_pipeline_succeeds
expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
end
- end
- end
- describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do
- let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
+ it 'sets the MR to merge when the pipeline succeeds' do
+ service = double(:merge_when_pipeline_succeeds_service)
- context 'when enabled' do
- before do
- project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
+ expect(MergeRequests::MergeWhenPipelineSucceedsService)
+ .to receive(:new).with(project, anything, anything)
+ .and_return(service)
+ expect(service).to receive(:execute).with(merge_request)
+
+ merge_when_pipeline_succeeds
end
- context 'with unresolved discussion' do
+ context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do
before do
- expect(merge_request).not_to be_discussions_resolved
+ project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
end
- it 'returns :failed' do
- merge_with_sha
+ it 'returns :merge_when_pipeline_succeeds' do
+ merge_when_pipeline_succeeds
- expect(assigns(:status)).to eq(:failed)
+ expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
end
end
+ end
+
+ describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do
+ let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
- context 'with all discussions resolved' do
+ context 'when enabled' do
before do
- merge_request.discussions.each { |d| d.resolve!(user) }
- expect(merge_request).to be_discussions_resolved
+ project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
end
- it 'returns :success' do
- merge_with_sha
+ context 'with unresolved discussion' do
+ before do
+ expect(merge_request).not_to be_discussions_resolved
+ end
+
+ it 'returns :failed' do
+ merge_with_sha
- expect(assigns(:status)).to eq(:success)
+ expect(assigns(:status)).to eq(:failed)
+ end
end
- end
- end
- context 'when disabled' do
- before do
- project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
+ context 'with all discussions resolved' do
+ before do
+ merge_request.discussions.each { |d| d.resolve!(user) }
+ expect(merge_request).to be_discussions_resolved
+ end
+
+ it 'returns :success' do
+ merge_with_sha
+
+ expect(assigns(:status)).to eq(:success)
+ end
+ end
end
- context 'with unresolved discussion' do
+ context 'when disabled' do
before do
- expect(merge_request).not_to be_discussions_resolved
+ project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
end
- it 'returns :success' do
- merge_with_sha
+ context 'with unresolved discussion' do
+ before do
+ expect(merge_request).not_to be_discussions_resolved
+ end
- expect(assigns(:status)).to eq(:success)
- end
- end
+ it 'returns :success' do
+ merge_with_sha
- context 'with all discussions resolved' do
- before do
- merge_request.discussions.each { |d| d.resolve!(user) }
- expect(merge_request).to be_discussions_resolved
+ expect(assigns(:status)).to eq(:success)
+ end
end
- it 'returns :success' do
- merge_with_sha
+ context 'with all discussions resolved' do
+ before do
+ merge_request.discussions.each { |d| d.resolve!(user) }
+ expect(merge_request).to be_discussions_resolved
+ end
+
+ it 'returns :success' do
+ merge_with_sha
- expect(assigns(:status)).to eq(:success)
+ expect(assigns(:status)).to eq(:success)
+ end
end
end
end
@@ -1111,16 +1282,102 @@ def go(format: 'html')
end
context 'POST remove_wip' do
- it 'removes the wip status' do
+ before do
merge_request.title = merge_request.wip_title
merge_request.save
+ end
- post :remove_wip,
- namespace_id: merge_request.project.namespace.to_param,
- project_id: merge_request.project,
- id: merge_request.iid
+ context 'as HTML' do
+ before do
+ post :remove_wip,
+ namespace_id: merge_request.project.namespace.to_param,
+ project_id: merge_request.project,
+ id: merge_request.iid
+ end
+
+ it 'removes the wip status' do
+ expect(merge_request.reload.title).to eq(merge_request.wipless_title)
+ end
+
+ it 'redirect to merge request show page' do
+ expect(response).to redirect_to(
+ namespace_project_merge_request_path(merge_request.project.namespace,
+ merge_request.project,
+ merge_request))
+ end
+ end
+
+ context 'as JSON' do
+ before do
+ xhr :post, :remove_wip,
+ namespace_id: merge_request.project.namespace.to_param,
+ project_id: merge_request.project,
+ id: merge_request.iid,
+ format: :json
+ end
+
+ it 'removes the wip status' do
+ expect(merge_request.reload.title).to eq(merge_request.wipless_title)
+ end
+
+ it 'renders MergeRequest as JSON' do
+ expect(json_response.keys).to include('id', 'iid', 'description')
+ end
+ end
+ end
+
+ describe 'POST cancel_merge_when_pipeline_succeeds' do
+ context 'as JS' do
+ subject do
+ xhr :post, :cancel_merge_when_pipeline_succeeds,
+ namespace_id: merge_request.project.namespace.to_param,
+ project_id: merge_request.project,
+ id: merge_request.iid
+ end
+
+ it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
+ mwps_service = double
+
+ allow(MergeRequests::MergeWhenPipelineSucceedsService)
+ .to receive(:new)
+ .and_return(mwps_service)
+
+ expect(mwps_service).to receive(:cancel).with(merge_request)
+
+ subject
+ end
+
+ it { is_expected.to render_template('cancel_merge_when_pipeline_succeeds') }
+ end
+
+ context 'as JSON' do
+ subject do
+ xhr :post, :cancel_merge_when_pipeline_succeeds,
+ namespace_id: merge_request.project.namespace.to_param,
+ project_id: merge_request.project,
+ id: merge_request.iid,
+ format: :json
+ end
+
+ it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
+ mwps_service = double
+
+ allow(MergeRequests::MergeWhenPipelineSucceedsService)
+ .to receive(:new)
+ .and_return(mwps_service)
- expect(merge_request.reload.title).to eq(merge_request.wipless_title)
+ expect(mwps_service).to receive(:cancel).with(merge_request)
+
+ subject
+ end
+
+ it { is_expected.to have_http_status(:success) }
+
+ it 'renders MergeRequest as JSON' do
+ subject
+
+ expect(json_response.keys).to include('id', 'iid', 'description')
+ end
end
end
diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js
index 82b00b4c1ec614c9a4fe8abeefb82abd3275bba5..10a60620f499a37700871d215e92812483eda519 100644
--- a/spec/javascripts/commit/pipelines/mock_data.js
+++ b/spec/javascripts/commit/pipelines/mock_data.js
@@ -61,6 +61,7 @@ export default {
tag: false,
branch: true,
},
+ coverage: '42.21',
commit: {
id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4',
short_id: 'fbd79f04',
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..6776c36c766a6e967da8d0fd495bffc6fa2a25dc
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author';
+
+const author = {
+ webUrl: 'http://foo.bar',
+ avatarUrl: 'http://gravatar.com/foo',
+ name: 'fatihacet',
+};
+const createComponent = () => {
+ const Component = Vue.extend(authorComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { author },
+ });
+};
+
+describe('MRWidgetAuthor', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const authorProp = authorComponent.props.author;
+
+ expect(authorProp).toBeDefined();
+ expect(authorProp.type instanceof Object).toBeTruthy();
+ expect(authorProp.required).toBeTruthy();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = createComponent().$el;
+
+ expect(el.tagName).toEqual('A');
+ expect(el.getAttribute('href')).toEqual(author.webUrl);
+ expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl);
+ expect(el.querySelector('.author').innerText).toEqual(author.name);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..515ddcbb8755a27b907e1743710e0817a6b782a4
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
@@ -0,0 +1,61 @@
+import Vue from 'vue';
+import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time';
+
+const props = {
+ actionText: 'Merged by',
+ author: {
+ webUrl: 'http://foo.bar',
+ avatarUrl: 'http://gravatar.com/foo',
+ name: 'fatihacet',
+ },
+ dateTitle: '2017-03-23T23:02:00.807Z',
+ dateReadable: '12 hours ago',
+};
+const createComponent = () => {
+ const Component = Vue.extend(authorTimeComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: props,
+ });
+};
+
+describe('MRWidgetAuthorTime', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props;
+ const ActionTextClass = actionText.type;
+ const DateTitleClass = dateTitle.type;
+ const DateReadableClass = dateReadable.type;
+
+ expect(new ActionTextClass() instanceof String).toBeTruthy();
+ expect(actionText.required).toBeTruthy();
+
+ expect(author.type instanceof Object).toBeTruthy();
+ expect(author.required).toBeTruthy();
+
+ expect(new DateTitleClass() instanceof String).toBeTruthy();
+ expect(dateTitle.required).toBeTruthy();
+
+ expect(new DateReadableClass() instanceof String).toBeTruthy();
+ expect(dateReadable.required).toBeTruthy();
+ });
+ });
+
+ describe('components', () => {
+ it('should have components', () => {
+ expect(authorTimeComponent.components['mr-widget-author']).toBeDefined();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = createComponent().$el;
+
+ expect(el.tagName).toEqual('H4');
+ expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl);
+ expect(el.querySelector('time').innerText).toContain(props.dateReadable);
+ expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..745b64c0d09e70878d7d0247888d8a914626017b
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
@@ -0,0 +1,139 @@
+import Vue from 'vue';
+import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
+import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
+
+const deploymentMockData = [
+ {
+ id: 15,
+ name: 'review/diplo',
+ url: '/root/acets-review-apps/environments/15',
+ stop_url: '/root/acets-review-apps/environments/15/stop',
+ external_url: 'http://diplo.',
+ external_url_formatted: 'diplo.',
+ deployed_at: '2017-03-22T22:44:42.258Z',
+ deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+ },
+];
+const createComponent = () => {
+ const Component = Vue.extend(deploymentComponent);
+ const mr = {
+ deployments: deploymentMockData,
+ };
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ });
+};
+
+describe('MRWidgetDeployment', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = deploymentComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('computed', () => {
+ describe('svg', () => {
+ it('should have the proper SVG icon', () => {
+ const vm = createComponent(deploymentMockData);
+ expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_success);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ const vm = createComponent();
+ const deployment = deploymentMockData[0];
+
+ describe('formatDate', () => {
+ it('should work', () => {
+ const readable = gl.utils.getTimeago().format(deployment.deployed_at);
+ expect(vm.formatDate(deployment.deployed_at)).toEqual(readable);
+ });
+ });
+
+ describe('hasExternalUrls', () => {
+ it('should return true', () => {
+ expect(vm.hasExternalUrls(deployment)).toBeTruthy();
+ });
+
+ it('should return false when there is not enough information', () => {
+ expect(vm.hasExternalUrls()).toBeFalsy();
+ expect(vm.hasExternalUrls({ external_url: 'Diplo' })).toBeFalsy();
+ expect(vm.hasExternalUrls({ external_url_formatted: 'Diplo' })).toBeFalsy();
+ });
+ });
+
+ describe('hasDeploymentTime', () => {
+ it('should return true', () => {
+ expect(vm.hasDeploymentTime(deployment)).toBeTruthy();
+ });
+
+ it('should return false when there is not enough information', () => {
+ expect(vm.hasDeploymentTime()).toBeFalsy();
+ expect(vm.hasDeploymentTime({ deployed_at: 'Diplo' })).toBeFalsy();
+ expect(vm.hasDeploymentTime({ deployed_at_formatted: 'Diplo' })).toBeFalsy();
+ });
+ });
+
+ describe('hasDeploymentMeta', () => {
+ it('should return true', () => {
+ expect(vm.hasDeploymentMeta(deployment)).toBeTruthy();
+ });
+
+ it('should return false when there is not enough information', () => {
+ expect(vm.hasDeploymentMeta()).toBeFalsy();
+ expect(vm.hasDeploymentMeta({ url: 'Diplo' })).toBeFalsy();
+ expect(vm.hasDeploymentMeta({ name: 'Diplo' })).toBeFalsy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+ const [deployment] = deploymentMockData;
+
+ beforeEach(() => {
+ vm = createComponent(deploymentMockData);
+ el = vm.$el;
+ });
+
+ it('should render template elements correctly', () => {
+ expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
+ expect(el.querySelector('.js-icon-link')).toBeDefined();
+ expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(deployment.url);
+ expect(el.querySelector('.js-deploy-meta').innerText).toContain(deployment.name);
+ expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deployment.external_url);
+ expect(el.querySelector('.js-deploy-url').innerText).toContain(deployment.external_url_formatted);
+ expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.formatDate(deployment.deployed_at));
+ expect(el.querySelector('button')).toBeDefined();
+ });
+
+ it('should list multiple deployments', (done) => {
+ vm.mr.deployments.push(deployment);
+ vm.mr.deployments.push(deployment);
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.ci_widget').length).toEqual(3);
+ done();
+ });
+ });
+
+ it('should not have some elements when there is not enough data', (done) => {
+ vm.mr.deployments = [{}];
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.js-deploy-meta').length).toEqual(0);
+ expect(el.querySelectorAll('.js-deploy-url').length).toEqual(0);
+ expect(el.querySelectorAll('.js-deploy-time').length).toEqual(0);
+ expect(el.querySelectorAll('.button').length).toEqual(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..734b14c688e3559df774d938c76830074ba54b4d
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -0,0 +1,86 @@
+import Vue from 'vue';
+import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header';
+
+const createComponent = (mr) => {
+ const Component = Vue.extend(headerComponent);
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ });
+};
+
+describe('MRWidgetHeader', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = headerComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('computed', () => {
+ it('shouldShowCommitsBehindText', () => {
+ const vm = createComponent({ divergedCommitsCount: 12 });
+ expect(vm.shouldShowCommitsBehindText).toBeTruthy();
+
+ vm.mr.divergedCommitsCount = 0;
+ expect(vm.shouldShowCommitsBehindText).toBeFalsy();
+ });
+
+ it('commitsText', () => {
+ const vm = createComponent({ divergedCommitsCount: 12 });
+ expect(vm.commitsText).toEqual('commits');
+
+ vm.mr.divergedCommitsCount = 1;
+ expect(vm.commitsText).toEqual('commit');
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+ const mr = {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ };
+
+ beforeEach(() => {
+ vm = createComponent(mr);
+ el = vm.$el;
+ });
+
+ it('should render template elements correctly', () => {
+ expect(el.classList.contains('mr-source-target')).toBeTruthy();
+ expect(el.querySelectorAll('.label-branch')[0].textContent).toContain(mr.sourceBranch);
+ expect(el.querySelectorAll('.label-branch')[1].textContent).toContain(mr.targetBranch);
+ expect(el.querySelector('.diverged-commits-count').textContent).toContain('12 commits behind');
+
+ expect(el.textContent).toContain('Check out branch');
+ expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath);
+ expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath);
+ });
+
+ it('should not have right action links if the MR state is not open', (done) => {
+ vm.mr.isOpen = false;
+ Vue.nextTick(() => {
+ expect(el.textContent).not.toContain('Check out branch');
+ expect(el.querySelectorAll('.dropdown li a').length).toEqual(0);
+ done();
+ });
+ });
+
+ it('should not render diverged commits count if the MR has no diverged commits', (done) => {
+ vm.mr.divergedCommitsCount = null;
+ Vue.nextTick(() => {
+ expect(el.textContent).not.toContain('commits behind');
+ expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..4da4fc82c264bd88b6d9cc3b4c19887aa9c0824c
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help';
+
+const props = {
+ missingBranch: 'this-is-not-the-branch-you-are-looking-for',
+};
+const text = `If the ${props.missingBranch} branch exists in your local repository`;
+
+const createComponent = () => {
+ const Component = Vue.extend(mergeHelpComponent);
+ return new Component({
+ el: document.createElement('div'),
+ propsData: props,
+ });
+};
+
+describe('MRWidgetMergeHelp', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { missingBranch } = mergeHelpComponent.props;
+ const MissingBranchTypeClass = missingBranch.type;
+
+ expect(new MissingBranchTypeClass() instanceof String).toBeTruthy();
+ expect(missingBranch.required).toBeFalsy();
+ expect(missingBranch.default).toEqual('');
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+
+ beforeEach(() => {
+ vm = createComponent();
+ el = vm.$el;
+ });
+
+ it('should have the correct elements', () => {
+ expect(el.classList.contains('mr-widget-help')).toBeTruthy();
+ expect(el.textContent).toContain(text);
+ });
+
+ it('should not show missing branch name if missingBranch props is not provided', (done) => {
+ vm.missingBranch = null;
+ Vue.nextTick(() => {
+ expect(el.textContent).not.toContain(text);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0824f3250689e014f4c09b9dd387d312730a824c
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -0,0 +1,129 @@
+import Vue from 'vue';
+import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
+import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
+import pipelineMockData from '../../commit/pipelines/mock_data';
+
+const createComponent = (mr) => {
+ const Component = Vue.extend(pipelineComponent);
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ });
+};
+
+describe('MRWidgetPipeline', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = pipelineComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('components', () => {
+ it('should have components added', () => {
+ expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
+ expect(pipelineComponent.components['pipeline-status-icon']).toBeDefined();
+ });
+ });
+
+ describe('computed', () => {
+ describe('svg', () => {
+ it('should have the proper SVG icon', () => {
+ const vm = createComponent({ pipeline: pipelineMockData });
+
+ expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_failed);
+ });
+ });
+
+ describe('hasCIError', () => {
+ it('should return false when there is no CI error', () => {
+ const vm = createComponent({
+ pipeline: pipelineMockData,
+ hasCI: true,
+ ciStatus: 'success',
+ });
+
+ expect(vm.hasCIError).toBeFalsy();
+ });
+
+ it('should return true when there is a CI error', () => {
+ const vm = createComponent({
+ pipeline: pipelineMockData,
+ hasCI: true,
+ ciStatus: null,
+ });
+
+ expect(vm.hasCIError).toBeTruthy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ let vm;
+ let el;
+ const mr = {
+ pipeline: pipelineMockData,
+ hasCI: true,
+ ciStatus: 'failed',
+ };
+
+ beforeEach(() => {
+ vm = createComponent(mr);
+ el = vm.$el;
+ });
+
+ it('should render template elements correctly', () => {
+ expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
+ expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-failed').length).toEqual(1);
+ expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipelineMockData.id}`);
+ expect(el.innerText).toContain('failed');
+ expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipelineMockData.path);
+ expect(el.querySelectorAll('.stage-container').length).toEqual(1);
+ expect(el.querySelector('.js-ci-error')).toEqual(null);
+ expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipelineMockData.commit.commit_path);
+ expect(el.querySelector('.js-commit-link').textContent).toEqual(pipelineMockData.commit.short_id);
+ expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipelineMockData.coverage}%`);
+ });
+
+ it('should list multiple stages', (done) => {
+ const [stage] = pipelineMockData.details.stages;
+ vm.mr.pipeline.details.stages.push(stage);
+ vm.mr.pipeline.details.stages.push(stage);
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.stage-container button').length).toEqual(3);
+ done();
+ });
+ });
+
+ it('should not have stages when there is no stage', (done) => {
+ vm.mr.pipeline.details.stages = [];
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.stage-container button').length).toEqual(0);
+ done();
+ });
+ });
+
+ it('should not have coverage text when pipeline has no coverage info', (done) => {
+ vm.mr.pipeline.coverage = null;
+
+ Vue.nextTick(() => {
+ expect(el.querySelector('.js-mr-coverage')).toEqual(null);
+ done();
+ });
+ });
+
+ it('should show CI error when there is a CI error', (done) => {
+ vm.mr.ciStatus = null;
+
+ Vue.nextTick(() => {
+ expect(el.querySelectorAll('.js-ci-error').length).toEqual(1);
+ expect(el.innerText).toContain('Could not connect to the CI server');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..c8366e87871650c1cc41fb2ac6752d5a40086dff
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links';
+
+const createComponent = (data) => {
+ const Component = Vue.extend(relatedLinksComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: data,
+ });
+};
+
+describe('MRWidgetRelatedLinks', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { relatedLinks } = relatedLinksComponent.props;
+
+ expect(relatedLinks).toBeDefined();
+ expect(relatedLinks.type instanceof Object).toBeTruthy();
+ expect(relatedLinks.required).toBeTruthy();
+ });
+ });
+
+ describe('methods', () => {
+ const data = {
+ relatedLinks: {
+ closing: '
#23 and
#42',
+ mentioned: '
#7',
+ },
+ };
+ const vm = createComponent(data);
+
+ describe('hasMultipleIssues', () => {
+ it('should return true if the given text has multiple issues', () => {
+ expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy();
+ });
+
+ it('should return false if the given text has one issue', () => {
+ expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy();
+ });
+ });
+
+ describe('issueLabel', () => {
+ it('should return true if the given text has multiple issues', () => {
+ expect(vm.issueLabel('closing')).toEqual('issues');
+ });
+
+ it('should return false if the given text has one issue', () => {
+ expect(vm.issueLabel('mentioned')).toEqual('issue');
+ });
+ });
+
+ describe('verbLabel', () => {
+ it('should return true if the given text has multiple issues', () => {
+ expect(vm.verbLabel('closing')).toEqual('are');
+ });
+
+ it('should return false if the given text has one issue', () => {
+ expect(vm.verbLabel('mentioned')).toEqual('is');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should have only have closing issues text', () => {
+ const vm = createComponent({ relatedLinks: { closing: '
#23 and
#42' } });
+
+ expect(vm.$el.innerText).toContain('Closes issues #23 and #42');
+ expect(vm.$el.innerText).not.toContain('mentioned');
+ });
+
+ it('should have only have mentioned issues text', () => {
+ const vm = createComponent({ relatedLinks: { mentioned: '
#7' } });
+
+ expect(vm.$el.innerText).toContain('issue #7');
+ expect(vm.$el.innerText).toContain('is mentioned but will not be closed.');
+ expect(vm.$el.innerText).not.toContain('Closes');
+ });
+
+ it('should have closing and mentioned issues at the same time', () => {
+ const vm = createComponent({
+ relatedLinks: {
+ closing: '
#7',
+ mentioned: '
#23 and
#42',
+ },
+ });
+
+ expect(vm.$el.innerText).toContain('Closes issue #7.');
+ expect(vm.$el.innerText).toContain('issues #23 and #42');
+ expect(vm.$el.innerText).toContain('are mentioned but will not be closed.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..cac2f561a0b373b0f2786c6f738a3db2e4ec9d86
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived';
+
+describe('MRWidgetArchived', () => {
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const Component = Vue.extend(archivedComponent);
+ const el = new Component({
+ el: document.createElement('div'),
+ }).$el;
+
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
+ expect(el.querySelector('button').disabled).toBeTruthy();
+ expect(el.innerText).toContain('This project is archived, write access has been disabled.');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..3be11d47227fe95fe9f20c72bf8c1c7530e57b88
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking';
+
+describe('MRWidgetChecking', () => {
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const Component = Vue.extend(checkingComponent);
+ const el = new Component({
+ el: document.createElement('div'),
+ }).$el;
+
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
+ expect(el.querySelector('button').disabled).toBeTruthy();
+ expect(el.innerText).toContain('Checking ability to merge automatically.');
+ expect(el.querySelector('i')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..47303d1e80fa620dd4ccf10c72dbdbf9bea0c96d
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed';
+
+const mr = {
+ targetBranch: 'good-branch',
+ targetBranchPath: '/good-branch',
+ closedBy: {
+ name: 'Fatih Acet',
+ username: 'fatihacet',
+ },
+ updatedAt: '2017-03-23T20:08:08.845Z',
+ closedAt: '1 day ago',
+};
+
+const createComponent = () => {
+ const Component = Vue.extend(closedComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ }).$el;
+};
+
+describe('MRWidgetClosed', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const mrProp = closedComponent.props.mr;
+
+ expect(mrProp.type instanceof Object).toBeTruthy();
+ expect(mrProp.required).toBeTruthy();
+ });
+ });
+
+ describe('components', () => {
+ it('should have components added', () => {
+ expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = createComponent();
+
+ expect(el.querySelector('h4').textContent).toContain('Closed by');
+ expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name);
+ expect(el.textContent).toContain('The changes were not merged into');
+ expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
+ expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a637c606675b5d9935b8bfd01c8faad26b72967d
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts';
+
+const path = '/conflicts';
+const createComponent = () => {
+ const Component = Vue.extend(conflictsComponent);
+
+ return new Component({
+ el: document.createElement('div'),
+ propsData: {
+ mr: {
+ canMerge: true,
+ canResolveConflicts: true,
+ canResolveConflictsInUI: true,
+ conflictResolutionPath: path,
+ },
+ },
+ });
+};
+
+describe('MRWidgetConflicts', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = conflictsComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('computed', () => {
+ it('showResolveConflictsButton', () => {
+ const vm = createComponent();
+ expect(vm.showResolveConflictsButton).toBeTruthy();
+
+ vm.mr.canMerge = false;
+ expect(vm.showResolveConflictsButton).toBeFalsy();
+
+ vm.mr.canMerge = true;
+ vm.mr.canResolveConflicts = false;
+ expect(vm.showResolveConflictsButton).toBeFalsy();
+
+ vm.mr.canMerge = true;
+ vm.mr.canResolveConflicts = true;
+ vm.mr.canResolveConflictsInUI = false;
+ expect(vm.showResolveConflictsButton).toBeFalsy();
+
+ vm.mr.canMerge = false;
+ vm.mr.canResolveConflicts = false;
+ vm.mr.canResolveConflictsInUI = false;
+ expect(vm.showResolveConflictsButton).toBeFalsy();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const el = createComponent().$el;
+ const resolveButton = el.querySelectorAll('.btn-group .btn')[0];
+ const mergeLocallyButton = el.querySelectorAll('.btn-group .btn')[1];
+
+ expect(el.textContent).toContain('There are merge conflicts.');
+ expect(el.textContent).not.toContain('ask someone with write access');
+ expect(el.querySelector('.btn-success').disabled).toBeTruthy();
+ expect(el.querySelectorAll('.btn-group .btn').length).toBe(2);
+ expect(resolveButton.textContent).toContain('Resolve conflicts');
+ expect(resolveButton.getAttribute('href')).toEqual(path);
+ expect(mergeLocallyButton.textContent).toContain('Merge locally');
+ });
+
+ describe('when user does not have permission to merge', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ vm.mr.canMerge = false;
+ });
+
+ it('should show proper message', (done) => {
+ Vue.nextTick(() => {
+ expect(vm.$el.textContent).toContain('ask someone with write access');
+ done();
+ });
+ });
+
+ it('should not have action buttons', (done) => {
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
+ expect(vm.$el.querySelector('a.js-resolve-conflicts-button')).toEqual(null);
+ expect(vm.$el.querySelector('a.js-merge-locally-button')).toEqual(null);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a043ee888a21add053fc8da065712e477aa4c4ec
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import lockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_locked';
+
+describe('MRWidgetLocked', () => {
+ describe('props', () => {
+ it('should have props', () => {
+ const { mr } = lockedComponent.props;
+
+ expect(mr.type instanceof Object).toBeTruthy();
+ expect(mr.required).toBeTruthy();
+ });
+ });
+
+ describe('template', () => {
+ it('should have correct elements', () => {
+ const Component = Vue.extend(lockedComponent);
+ const mr = {
+ targetBranchPath: '/branch-path',
+ targetBranch: 'branch',
+ };
+ const el = new Component({
+ el: document.createElement('div'),
+ propsData: { mr },
+ }).$el;
+
+ expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(el.innerText).toContain('it is locked');
+ expect(el.innerText).toContain('changes will be merged into');
+ expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
+ expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
+ });
+ });
+});
diff --git a/spec/serializers/event_entity_spec.rb b/spec/serializers/event_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bb54597c96742a1d4a4019cc1e9c9db6c3620df2
--- /dev/null
+++ b/spec/serializers/event_entity_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe EventEntity do
+ subject { described_class.represent(create(:event)).as_json }
+
+ it 'exposes author' do
+ expect(subject).to include(:author)
+ end
+
+ it 'exposes core elements of event' do
+ expect(subject).to include(:updated_at)
+ end
+end
diff --git a/spec/serializers/merge_request_basic_serializer_spec.rb b/spec/serializers/merge_request_basic_serializer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4daf5a59d0ccf9498a6e49cd3ca608d50f6c8628
--- /dev/null
+++ b/spec/serializers/merge_request_basic_serializer_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe MergeRequestBasicSerializer do
+ let(:resource) { create(:merge_request) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new.represent(resource) }
+
+ it 'has important MergeRequest attributes' do
+ expect(subject).to include(:merge_status)
+ end
+end
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9d7baeaabf4bd50b568f7495e2c5942d69f2f7d5
--- /dev/null
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -0,0 +1,269 @@
+require 'spec_helper'
+
+describe MergeRequestSerializer do
+ let(:project) { create :empty_project }
+ let(:resource) { create(:merge_request, source_project: project, target_project: project) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new(current_user: user).represent(resource) }
+
+ it 'includes author' do
+ req = double('request')
+
+ author_payload = UserEntity
+ .represent(resource.author, request: req)
+ .as_json
+
+ expect(subject[:author]).to eql(author_payload)
+ end
+
+ it 'includes pipeline' do
+ req = double('request', current_user: user)
+ pipeline = build_stubbed(:ci_pipeline)
+ allow(resource).to receive(:head_pipeline).and_return(pipeline)
+
+ pipeline_payload = PipelineEntity
+ .represent(pipeline, request: req)
+ .as_json
+
+ expect(subject[:pipeline]).to eql(pipeline_payload)
+ end
+
+ it 'has important MergeRequest attributes' do
+ expect(subject).to include(:diff_head_sha, :merge_commit_message,
+ :can_be_merged, :can_be_cherry_picked,
+ :has_conflicts, :has_ci)
+ end
+
+ it 'has merge_path' do
+ expect(subject[:merge_path])
+ .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}/merge")
+ end
+
+ it 'has remove_wip_path' do
+ expect(subject[:remove_wip_path])
+ .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}/remove_wip")
+ end
+
+ it 'has conflict_resolution_ui_path' do
+ expect(subject[:conflict_resolution_ui_path])
+ .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}/conflicts")
+ end
+
+ it 'has email_patches_path' do
+ expect(subject[:email_patches_path])
+ .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}.patch")
+ end
+
+ it 'has plain_diff_path' do
+ expect(subject[:plain_diff_path])
+ .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}.diff")
+ end
+
+ it 'has target_branch_path' do
+ expect(subject[:target_branch_path])
+ .to eql("/#{resource.target_project.full_path}/branches/#{resource.target_branch}")
+ end
+
+ it 'has source_branch_path' do
+ expect(subject[:source_branch_path])
+ .to eql("/#{resource.source_project.full_path}/branches/#{resource.source_branch}")
+ end
+
+ it 'has merge_commit_message_with_description' do
+ expect(subject[:merge_commit_message_with_description])
+ .to eql(resource.merge_commit_message(include_description: true))
+ end
+
+ describe 'diff_head_commit_short_id' do
+ context 'when no diff head commit' do
+ let(:project) { create :empty_project }
+
+ it 'returns nil' do
+ expect(subject[:diff_head_commit_short_id]).to be_nil
+ end
+ end
+
+ context 'when diff head commit present' do
+ let(:project) { create :project }
+
+ it 'returns diff head commit short id' do
+ expect(subject[:diff_head_commit_short_id]).to eql(resource.diff_head_commit.short_id)
+ end
+ end
+ end
+
+ describe 'ci_status' do
+ let(:project) { create :project }
+
+ context 'when no head pipeline' do
+ it 'return status using CiService' do
+ ci_service = double(MockCiService)
+ ci_status = double
+
+ allow(resource.source_project)
+ .to receive(:ci_service)
+ .and_return(ci_service)
+
+ allow(resource).to receive(:head_pipeline).and_return(nil)
+
+
+ expect(ci_service).to receive(:commit_status)
+ .with(resource.diff_head_sha, resource.source_branch)
+ .and_return(ci_status)
+
+ expect(subject[:ci_status]).to eql(ci_status)
+ end
+ end
+
+ context 'when head pipeline present' do
+ let(:pipeline) { build_stubbed(:ci_pipeline) }
+
+ before do
+ allow(resource).to receive(:head_pipeline).and_return(pipeline)
+ end
+
+ context 'success with warnings' do
+ before do
+ allow(pipeline).to receive(:success?) { true }
+ allow(pipeline).to receive(:has_warnings?) { true }
+ end
+
+ it 'returns "success_with_warnings"' do
+ expect(subject[:ci_status]).to eql('success_with_warnings')
+ end
+ end
+
+ context 'pipeline HAS status AND its not success with warnings' do
+ before do
+ allow(pipeline).to receive(:success?) { false }
+ allow(pipeline).to receive(:has_warnings?) { false }
+ end
+
+ it 'returns pipeline status' do
+ expect(subject[:ci_status]).to eql('pending')
+ end
+ end
+
+ context 'pipeline has NO status AND its not success with warnings' do
+ before do
+ allow(pipeline).to receive(:status) { nil }
+ allow(pipeline).to receive(:success?) { false }
+ allow(pipeline).to receive(:has_warnings?) { false }
+ end
+
+ it 'returns "preparing"' do
+ expect(subject[:ci_status]).to eql('preparing')
+ end
+ end
+ end
+ end
+
+ it 'includes merge_event' do
+ event = create(:event, :merged, author: user, project: resource.project, target: resource)
+
+ event_payload = EventEntity
+ .represent(event)
+ .as_json
+
+ expect(subject[:merge_event]).to eql(event_payload)
+ end
+
+ it 'includes closed_event' do
+ event = create(:event, :closed, author: user, project: resource.project, target: resource)
+
+ event_payload = EventEntity
+ .represent(event)
+ .as_json
+
+ expect(subject[:closed_event]).to eql(event_payload)
+ end
+
+ describe 'diverged_commits_count' do
+ context 'when MR open and its diverging' do
+ it 'returns diverged commits count' do
+ allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: true,
+ diverged_commits_count: 10)
+
+ expect(subject[:diverged_commits_count]).to eql(10)
+ end
+ end
+
+ context 'when MR is not open' do
+ it 'returns 0' do
+ allow(resource).to receive_messages(open?: false)
+
+ expect(subject[:diverged_commits_count]).to be_zero
+ end
+ end
+
+ context 'when MR is not diverging' do
+ it 'returns 0' do
+ allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: false)
+
+ expect(subject[:diverged_commits_count]).to be_zero
+ end
+ end
+ end
+
+ context 'current_user' do
+ describe 'can_update_merge_request' do
+ context 'user can update issue' do
+ it 'returns true' do
+ resource.project.team << [user, :developer]
+
+ expect(subject[:current_user][:can_update_merge_request]).to eql(true)
+ end
+ end
+
+ context 'user cannot update issue' do
+ it 'returns false' do
+ expect(subject[:current_user][:can_update_merge_request]).to eql(false)
+ end
+ end
+ end
+ end
+
+ context 'issues_links' do
+ let(:project) { create(:project, :private, creator: user, namespace: user.namespace) }
+ let(:issue_a) { create(:issue, project: project) }
+ let(:issue_b) { create(:issue, project: project) }
+
+ let(:resource) do
+ create(:merge_request,
+ source_project: project, target_project: project,
+ description: "Fixes #{issue_a.to_reference} Related #{issue_b.to_reference}")
+ end
+
+ before do
+ project.team << [user, :developer]
+
+ allow(resource.project).to receive(:default_branch)
+ .and_return(resource.target_branch)
+ end
+
+ describe 'closing' do
+ let(:sentence) { subject[:issues_links][:closing] }
+
+ it 'presents closing issues links' do
+ expect(sentence).to match("#{project.full_path}/issues/#{issue_a.iid}")
+ end
+
+ it 'does not present related issues links' do
+ expect(sentence).not_to match("#{project.full_path}/issues/#{issue_b.iid}")
+ end
+ end
+
+ describe 'mentioned_but_not_closing' do
+ let(:sentence) { subject[:issues_links][:mentioned_but_not_closing] }
+
+ it 'presents related issues links' do
+ expect(sentence).to match("#{project.full_path}/issues/#{issue_b.iid}")
+ end
+
+ it 'does not present closing issues links' do
+ expect(sentence).not_to match("#{project.full_path}/issues/#{issue_a.iid}")
+ end
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 93d5a21419d80c275919dcebd7fc591b6a5f3bde..d2482ac434b48ba0881f3039fc1bd95c88d140f8 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -5,7 +5,7 @@
let(:request) { double('request') }
before do
- allow(request).to receive(:user).and_return(user)
+ allow(request).to receive(:current_user).and_return(user)
end
let(:entity) do
@@ -19,7 +19,7 @@
let(:pipeline) { create(:ci_empty_pipeline) }
it 'contains required fields' do
- expect(subject).to include :id, :user, :path
+ expect(subject).to include :id, :user, :path, :coverage
expect(subject).to include :ref, :commit
expect(subject).to include :updated_at, :created_at
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 8642b8038448ba11d88aea434bee082887f0fd2f..10a657e07e41f1b1079385f2e4a8767de9ba44e2 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -4,7 +4,7 @@
let(:user) { create(:user) }
let(:serializer) do
- described_class.new(user: user)
+ described_class.new(current_user: user)
end
subject { serializer.represent(resource) }
@@ -44,7 +44,7 @@
end
let(:serializer) do
- described_class.new(user: user)
+ described_class.new(current_user: user)
.with_pagination(request, response)
end
diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb
index 4ab40d08432a79e098ade6de1dc10964c26dfae3..bd7b706bb8d643b68199d218b0f294e82c2b0c04 100644
--- a/spec/serializers/stage_entity_spec.rb
+++ b/spec/serializers/stage_entity_spec.rb
@@ -14,7 +14,7 @@
end
before do
- allow(request).to receive(:user).and_return(user)
+ allow(request).to receive(:current_user).and_return(user)
create(:ci_build, :success, pipeline: pipeline)
end