merge_requests_controller.rb 11 KB
Newer Older
1
class Projects::MergeRequestsController < Projects::ApplicationController
2
  include ToggleSubscriptionAction
3
  include DiffHelper
4
  include IssuableActions
5
  include ToggleAwardEmoji
6

7
  before_action :module_enabled
8
  before_action :merge_request, only: [
9
    :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
10
    :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip
11
  ]
12 13 14
  before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
  before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
  before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
15
  before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
16
  before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
17 18

  # Allow read any merge_request
19
  before_action :authorize_read_merge_request!
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
20 21

  # Allow write(create) merge_request
22
  before_action :authorize_create_merge_request!, only: [:new, :create]
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
23 24

  # Allow modify merge_request
25
  before_action :authorize_update_merge_request!, only: [:close, :edit, :update, :remove_wip, :sort]
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
26

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
27
  def index
28
    terms = params['issue_search']
29
    @merge_requests = get_merge_requests_collection
30 31 32 33 34 35 36 37

    if terms.present?
      if terms =~ /\A[#!](\d+)\z/
        @merge_requests = @merge_requests.where(iid: $1)
      else
        @merge_requests = @merge_requests.full_search(terms)
      end
    end
38

39
    @merge_requests = @merge_requests.page(params[:page])
40
    @merge_requests = @merge_requests.preload(:target_project)
41

42
    @labels = @project.labels.where(title: params[:label_name])
Tap's avatar
Tap committed
43

44 45 46 47
    respond_to do |format|
      format.html
      format.json do
        render json: {
48
          html: view_to_html_string("projects/merge_requests/_merge_requests"),
49
          labels: @labels.as_json(methods: :text_color)
50 51 52
        }
      end
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
53 54 55
  end

  def show
56
    @note_counts = Note.where(commit_id: @merge_request.commits.map(&:id)).
57
      group(:commit_id).count
58

59 60
    respond_to do |format|
      format.html
61
      format.json { render json: @merge_request }
62 63
      format.diff { render text: @merge_request.to_diff }
      format.patch { render text: @merge_request.to_patch }
64
    end
randx's avatar
randx committed
65 66
  end

67
  def diffs
68 69
    apply_diff_view_cookie!

70
    @commit = @merge_request.last_commit
71 72 73 74 75
    @base_commit = @merge_request.diff_base_commit

    # MRs created before 8.4 don't have a diff_base_commit,
    # but we need it for the "View file @ ..." link by deleted files
    @base_commit ||= @merge_request.first_commit.parent || @merge_request.first_commit
76

77 78 79 80
    @comments_target = {
      noteable_type: 'MergeRequest',
      noteable_id: @merge_request.id
    }
81 82

    @grouped_diff_notes = @merge_request.notes.grouped_diff_notes
83

84 85 86 87
    respond_to do |format|
      format.html
      format.json { render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") } }
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
88 89
  end

90
  def commits
91 92 93 94
    respond_to do |format|
      format.html { render 'show' }
      format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_commits') } }
    end
95 96
  end

97 98 99 100 101 102 103
  def builds
    respond_to do |format|
      format.html { render 'show' }
      format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } }
    end
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
104
  def new
105 106
    params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
    @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
107
    @noteable = @merge_request
108 109 110 111 112 113 114 115 116

    @target_branches = if @merge_request.target_project
                         @merge_request.target_project.repository.branch_names
                       else
                         []
                       end

    @target_project = merge_request.target_project
    @source_project = merge_request.source_project
117
    @commits = @merge_request.compare_commits.reverse
118
    @commit = @merge_request.last_commit
119
    @base_commit = @merge_request.diff_base_commit
120
    @diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare
121
    @diff_notes_disabled = true
122 123 124 125

    @ci_commit = @merge_request.ci_commit
    @statuses = @ci_commit.statuses if @ci_commit

126 127
    @note_counts = Note.where(commit_id: @commits.map(&:id)).
      group(:commit_id).count
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
128 129 130
  end

  def create
131
    @target_branches ||= []
132
    @merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
133 134

    if @merge_request.valid?
135
      redirect_to(merge_request_path(@merge_request))
136
    else
Izaak Alpert's avatar
Izaak Alpert committed
137 138 139
      @source_project = @merge_request.source_project
      @target_project = @merge_request.target_project
      render action: "new"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
140 141 142
    end
  end

143 144 145 146 147 148
  def edit
    @source_project = @merge_request.source_project
    @target_project = @merge_request.target_project
    @target_branches = @merge_request.target_project.repository.branch_names
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
149
  def update
150
    @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
151

152
    if @merge_request.valid?
153 154
      respond_to do |format|
        format.html do
Vinnie Okada's avatar
Vinnie Okada committed
155
          redirect_to([@merge_request.target_project.namespace.becomes(Namespace),
156
                       @merge_request.target_project, @merge_request])
157
        end
158
        format.json do
159
          render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
160
        end
161
      end
162
    else
163
      render "edit"
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
164 165 166
    end
  end

167 168 169 170 171 172 173
  def remove_wip
    MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request)

    redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
      notice: "The merge request can now be merged."
  end

174
  def merge_check
175
    @merge_request.check_if_can_be_merged
176

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
177
    render partial: "projects/merge_requests/widget/show.html.haml", layout: false
Valery Sizov's avatar
Valery Sizov committed
178 179
  end

180
  def cancel_merge_when_build_succeeds
181
    return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
182

183
    MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request)
184 185
  end

186
  def merge
187
    return access_denied! unless @merge_request.can_be_merged_by?(current_user)
188

189 190 191 192 193
    unless @merge_request.mergeable?
      @status = :failed
      return
    end

194 195 196 197 198
    if params[:sha] != @merge_request.source_sha
      @status = :sha_mismatch
      return
    end

199
    TodoService.new.merge_merge_request(merge_request, current_user)
200

201 202
    @merge_request.update(merge_error: nil)

203
    if params[:merge_when_build_succeeds].present? && @merge_request.ci_commit && @merge_request.ci_commit.active?
204
      MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user, merge_params)
205
        .execute(@merge_request)
206
      @status = :merge_when_build_succeeds
207
    else
208 209
      MergeWorker.perform_async(@merge_request.id, current_user.id, params)
      @status = :success
210
    end
randx's avatar
randx committed
211 212
  end

213
  def branch_from
214
    # This is always source
Izaak Alpert's avatar
Izaak Alpert committed
215
    @source_project = @merge_request.nil? ? @project : @merge_request.source_project
216
    @commit = @repository.commit(params[:ref]) if params[:ref].present?
217
    render layout: false
218 219 220
  end

  def branch_to
221
    @target_project = selected_target_project
222
    @commit = @target_project.commit(params[:ref]) if params[:ref].present?
223
    render layout: false
224 225
  end

226 227
  def update_branches
    @target_project = selected_target_project
Izaak Alpert's avatar
Izaak Alpert committed
228
    @target_branches = @target_project.repository.branch_names
229

Phil Hughes's avatar
Phil Hughes committed
230
    render layout: false
231 232
  end

233
  def ci_status
234 235
    ci_commit = @merge_request.ci_commit
    if ci_commit
Phil Hughes's avatar
Phil Hughes committed
236
      status = ci_commit.status
237
      coverage = ci_commit.try(:coverage)
238 239

      status ||= "preparing"
240 241 242
    else
      ci_service = @merge_request.source_project.ci_service
      status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch) if ci_service
243

244 245 246
      if ci_service.respond_to?(:commit_coverage)
        coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch)
      end
247 248 249
    end

    response = {
250 251
      title: merge_request.title,
      sha: merge_request.last_commit_short_sha,
252 253 254
      status: status,
      coverage: coverage
    }
255 256 257 258

    render json: response
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
259 260
  protected

261
  def selected_target_project
262 263 264 265 266
    if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil?
      @project
    else
      @project.forked_project_link.forked_from_project
    end
267 268
  end

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
269
  def merge_request
skv's avatar
skv committed
270
    @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
271
  end
272
  alias_method :subscribable_resource, :merge_request
273
  alias_method :issuable, :merge_request
274
  alias_method :awardable, :merge_request
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
275

276 277 278 279
  def closes_issues
    @closes_issues ||= @merge_request.closes_issues
  end

280
  def authorize_update_merge_request!
281
    return render_404 unless can?(current_user, :update_merge_request, @merge_request)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
282 283 284
  end

  def authorize_admin_merge_request!
285
    return render_404 unless can?(current_user, :admin_merge_request, @merge_request)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
286
  end
287 288 289 290

  def module_enabled
    return render_404 unless @project.merge_requests_enabled
  end
291 292

  def validates_merge_request
293 294 295
    # If source project was removed (Ex. mr from fork to origin)
    return invalid_mr unless @merge_request.source_project

296 297 298
    # Show git not found page
    # if there is no saved commits between source & target branch
    if @merge_request.commits.blank?
299 300
      # and if target branch doesn't exist
      return invalid_mr unless @merge_request.target_branch_exists?
301

302 303
      # or if source branch doesn't exist
      return invalid_mr unless @merge_request.source_branch_exists?
304
    end
305 306 307 308
  end

  def define_show_vars
    # Build a note object for comment form
309
    @note = @project.notes.new(noteable: @merge_request)
310
    @notes = @merge_request.mr_and_commit_notes.inc_author.fresh
ZJ van de Weg's avatar
ZJ van de Weg committed
311
    @discussions = @notes.discussions
312
    @noteable = @merge_request
313

314
    # Get commits from repository
315 316
    # or from cache if already merged
    @commits = @merge_request.commits
317

318
    @merge_request_diff = @merge_request.merge_request_diff
319

320
    @ci_commit = @merge_request.ci_commit
321
    @statuses = @ci_commit.statuses if @ci_commit
322

323 324 325 326
    if @merge_request.locked_long_ago?
      @merge_request.unlock_mr
      @merge_request.close
    end
327 328
  end

329 330
  def define_widget_vars
    @ci_commit = @merge_request.ci_commit
331
    @ci_commits = [@ci_commit].compact
332
    closes_issues
333 334
  end

335 336 337 338 339
  def invalid_mr
    # Render special view for MR with removed source or target branch
    render 'invalid'
  end

340
  def merge_request_params
341
    params.require(:merge_request).permit(
342 343
      :title, :assignee_id, :source_project_id, :source_branch,
      :target_project_id, :target_branch, :milestone_id,
344 345
      :state_event, :description, :task_num, :force_remove_source_branch,
      label_ids: []
346 347
    )
  end
348

349 350 351 352
  def merge_params
    params.permit(:should_remove_source_branch, :commit_message)
  end

353 354 355 356 357
  # Make sure merge requests created before 8.0
  # have head file in refs/merge-requests/
  def ensure_ref_fetched
    @merge_request.ensure_ref_fetched
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
358
end