merge_request_spec.rb 68.9 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1
2
require 'spec_helper'

3
describe MergeRequest do
4
  include RepoHelpers
5
  include ProjectForksHelper
6

7
8
  subject { create(:merge_request) }

Robert Speicher's avatar
Robert Speicher committed
9
  describe 'associations' do
10
11
    it { is_expected.to belong_to(:target_project).class_name('Project') }
    it { is_expected.to belong_to(:source_project).class_name('Project') }
12
    it { is_expected.to belong_to(:merge_user).class_name("User") }
13
    it { is_expected.to belong_to(:assignee) }
14
    it { is_expected.to have_many(:merge_request_diffs) }
Robert Speicher's avatar
Robert Speicher committed
15
16
  end

17
18
19
  describe 'modules' do
    subject { described_class }

20
    it { is_expected.to include_module(NonatomicInternalId) }
21
22
23
24
25
26
27
    it { is_expected.to include_module(Issuable) }
    it { is_expected.to include_module(Referable) }
    it { is_expected.to include_module(Sortable) }
    it { is_expected.to include_module(Taskable) }
  end

  describe 'validation' do
28
29
    it { is_expected.to validate_presence_of(:target_branch) }
    it { is_expected.to validate_presence_of(:source_branch) }
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
30

31
    context "Validation of merge user with Merge When Pipeline Succeeds" do
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
32
33
34
35
36
      it "allows user to be nil when the feature is disabled" do
        expect(subject).to be_valid
      end

      it "is invalid without merge user" do
37
        subject.merge_when_pipeline_succeeds = true
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
38
39
40
41
        expect(subject).not_to be_valid
      end

      it "is valid with merge user" do
42
        subject.merge_when_pipeline_succeeds = true
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
43
44
45
46
47
        subject.merge_user = build(:user)

        expect(subject).to be_valid
      end
    end
48
49
50

    context 'for forks' do
      let(:project) { create(:project) }
51
52
      let(:fork1) { fork_project(project) }
      let(:fork2) { fork_project(project) }
53
54
55
56
57
58
59
60

      it 'allows merge requests for sibling-forks' do
        subject.source_project = fork1
        subject.target_project = fork2

        expect(subject).to be_valid
      end
    end
61
62
  end

63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
  describe 'callbacks' do
    describe '#ensure_merge_request_metrics' do
      it 'creates metrics after saving' do
        merge_request = create(:merge_request)

        expect(merge_request.metrics).to be_persisted
        expect(MergeRequest::Metrics.count).to eq(1)
      end

      it 'does not duplicate metrics for a merge request' do
        merge_request = create(:merge_request)

        merge_request.mark_as_merged!

        expect(MergeRequest::Metrics.count).to eq(1)
      end
    end
  end

Robert Speicher's avatar
Robert Speicher committed
82
  describe 'respond to' do
83
84
85
    it { is_expected.to respond_to(:unchecked?) }
    it { is_expected.to respond_to(:can_be_merged?) }
    it { is_expected.to respond_to(:cannot_be_merged?) }
86
    it { is_expected.to respond_to(:merge_params) }
87
    it { is_expected.to respond_to(:merge_when_pipeline_succeeds) }
88
  end
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
89

90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
  describe '.by_commit_sha' do
    subject(:by_commit_sha) { described_class.by_commit_sha(sha) }

    let!(:merge_request) { create(:merge_request, :with_diffs) }

    context 'with sha contained in latest merge request diff' do
      let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }

      it 'returns merge requests' do
        expect(by_commit_sha).to eq([merge_request])
      end
    end

    context 'with sha contained not in latest merge request diff' do
      let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }

      it 'returns empty requests' do
        latest_merge_request_diff = merge_request.merge_request_diffs.create
        latest_merge_request_diff.merge_request_diff_commits.where(sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0').delete_all

        expect(by_commit_sha).to be_empty
      end
    end

    context 'with sha not contained in' do
      let(:sha) { 'b83d6e3' }

      it 'returns empty result' do
        expect(by_commit_sha).to be_empty
      end
    end
  end

123
124
125
126
127
128
  describe '.in_projects' do
    it 'returns the merge requests for a set of projects' do
      expect(described_class.in_projects(Project.all)).to eq([subject])
    end
  end

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
  describe '.set_latest_merge_request_diff_ids!' do
    def create_merge_request_with_diffs(source_branch, diffs: 2)
      params = {
        target_project: project,
        target_branch: 'master',
        source_project: project,
        source_branch: source_branch
      }

      create(:merge_request, params).tap do |mr|
        diffs.times { mr.merge_request_diffs.create }
      end
    end

    let(:project) { create(:project) }

    it 'sets IDs for merge requests, whether they are already set or not' do
      merge_requests = [
        create_merge_request_with_diffs('feature'),
        create_merge_request_with_diffs('feature-conflict'),
        create_merge_request_with_diffs('wip', diffs: 0),
        create_merge_request_with_diffs('csv')
      ]

      merge_requests.take(2).each do |merge_request|
        merge_request.update_column(:latest_merge_request_diff_id, nil)
      end

      expected = merge_requests.map do |merge_request|
        merge_request.merge_request_diffs.maximum(:id)
      end

      expect { project.merge_requests.set_latest_merge_request_diff_ids! }
        .to change { merge_requests.map { |mr| mr.reload.latest_merge_request_diff_id } }.to(expected)
    end
  end

166
  describe '#target_branch_sha' do
167
    let(:project) { create(:project, :repository) }
168

169
    subject { create(:merge_request, source_project: project, target_project: project) }
170

171
    context 'when the target branch does not exist' do
172
      before do
173
        project.repository.rm_branch(subject.author, subject.target_branch)
174
        subject.clear_memoized_shas
175
      end
176
177

      it 'returns nil' do
178
        expect(subject.target_branch_sha).to be_nil
179
180
      end
    end
181
182
183
184
185
186

    it 'returns memoized value' do
      subject.target_branch_sha = '8ffb3c15a5475e59ae909384297fede4badcb4c7'

      expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7'
    end
187
188
  end

189
190
191
192
193
  describe '#card_attributes' do
    it 'includes the author name' do
      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
      allow(subject).to receive(:assignee).and_return(nil)

194
195
      expect(subject.card_attributes)
        .to eq({ 'Author' => 'Robert', 'Assignee' => nil })
196
197
198
199
200
201
    end

    it 'includes the assignee name' do
      allow(subject).to receive(:author).and_return(double(name: 'Robert'))
      allow(subject).to receive(:assignee).and_return(double(name: 'Douwe'))

202
203
      expect(subject.card_attributes)
        .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
204
205
206
    end
  end

207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
  describe '#assignee_ids' do
    it 'returns an array of the assigned user id' do
      subject.assignee_id = 123

      expect(subject.assignee_ids).to eq([123])
    end
  end

  describe '#assignee_ids=' do
    it 'sets assignee_id to the last id in the array' do
      subject.assignee_ids = [123, 456]

      expect(subject.assignee_id).to eq(456)
    end
  end

223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
  describe '#assignee_or_author?' do
    let(:user) { create(:user) }

    it 'returns true for a user that is assigned to a merge request' do
      subject.assignee = user

      expect(subject.assignee_or_author?(user)).to eq(true)
    end

    it 'returns true for a user that is the author of a merge request' do
      subject.author = user

      expect(subject.assignee_or_author?(user)).to eq(true)
    end

    it 'returns false for a user that is not the assignee or author' do
      expect(subject.assignee_or_author?(user)).to eq(false)
    end
  end

243
244
  describe '#cache_merge_request_closes_issues!' do
    before do
245
      subject.project.add_developer(subject.author)
246
247
248
249
250
251
252
253
      subject.target_branch = subject.project.default_branch
    end

    it 'caches closed issues' do
      issue  = create :issue, project: subject.project
      commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
      allow(subject).to receive(:commits).and_return([commit])

254
      expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
255
256
    end

257
258
259
260
    context 'when both internal and external issue trackers are enabled' do
      before do
        subject.project.has_external_issue_tracker = true
        subject.project.save!
261
        create(:jira_service, project: subject.project)
262
263
264
265
266
267
      end

      it 'does not cache issues from external trackers' do
        issue  = ExternalIssue.new('JIRA-123', subject.project)
        commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
        allow(subject).to receive(:commits).and_return([commit])
268

269
        expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
        expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
      end

      it 'caches an internal issue' do
        issue  = create(:issue, project: subject.project)
        commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
        allow(subject).to receive(:commits).and_return([commit])

        expect { subject.cache_merge_request_closes_issues!(subject.author) }
          .to change(subject.merge_requests_closing_issues, :count).by(1)
      end
    end

    context 'when only external issue tracker enabled' do
      before do
        subject.project.has_external_issue_tracker = true
        subject.project.issues_enabled = false
        subject.project.save!
      end

      it 'does not cache issues from external trackers' do
        issue  = ExternalIssue.new('JIRA-123', subject.project)
        commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
        allow(subject).to receive(:commits).and_return([commit])

        expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
      end

      it 'does not cache an internal issue' do
        issue  = create(:issue, project: subject.project)
        commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
        allow(subject).to receive(:commits).and_return([commit])

        expect { subject.cache_merge_request_closes_issues!(subject.author) }
          .not_to change(subject.merge_requests_closing_issues, :count)
      end
306
307
308
    end
  end

309
  describe '#source_branch_sha' do
310
    let(:last_branch_commit) { subject.source_project.repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + subject.source_branch) }
311
312
313
314

    context 'with diffs' do
      subject { create(:merge_request, :with_diffs) }
      it 'returns the sha of the source branch last commit' do
315
        expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
316
317
318
      end
    end

319
320
321
    context 'without diffs' do
      subject { create(:merge_request, :without_diffs) }
      it 'returns the sha of the source branch last commit' do
322
        expect(subject.source_branch_sha).to eq(last_branch_commit.sha)
323
      end
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338

      context 'when there is a tag name matching the branch name' do
        let(:tag_name) { subject.source_branch }

        it 'returns the sha of the source branch last commit' do
          subject.source_project.repository.add_tag(subject.author,
                                                    tag_name,
                                                    subject.target_branch_sha,
                                                    'Add a tag')

          expect(subject.source_branch_sha).to eq(last_branch_commit.sha)

          subject.source_project.repository.rm_tag(subject.author, tag_name)
        end
      end
339
340
    end

341
342
343
    context 'when the merge request is being created' do
      subject { build(:merge_request, source_branch: nil, compare_commits: []) }
      it 'returns nil' do
344
        expect(subject.source_branch_sha).to be_nil
345
346
      end
    end
347
348
349
350
351
352

    it 'returns memoized value' do
      subject.source_branch_sha = '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'

      expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b'
    end
353
354
  end

355
  describe '#to_reference' do
356
    let(:project) { build(:project, name: 'sample-project') }
357
358
    let(:merge_request) { build(:merge_request, target_project: project, iid: 1) }

359
    it 'returns a String reference to the object' do
360
      expect(merge_request.to_reference).to eq "!1"
361
362
363
    end

    it 'supports a cross-project reference' do
364
      another_project = build(:project, name: 'another-project', namespace: project.namespace)
365
      expect(merge_request.to_reference(another_project)).to eq "sample-project!1"
366
    end
367
368

    it 'returns a String reference with the full path' do
369
      expect(merge_request.to_reference(full: true)).to eq(project.full_path + '!1')
370
    end
371
  end
372

373
  describe '#raw_diffs' do
374
375
376
377
378
379
380
    let(:merge_request) { build(:merge_request) }
    let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }

    context 'when there are MR diffs' do
      it 'delegates to the MR diffs' do
        merge_request.merge_request_diff = MergeRequestDiff.new

381
        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(options)
382

383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
        merge_request.raw_diffs(options)
      end
    end

    context 'when there are no MR diffs' do
      it 'delegates to the compare object' do
        merge_request.compare = double(:compare)

        expect(merge_request.compare).to receive(:raw_diffs).with(options)

        merge_request.raw_diffs(options)
      end
    end
  end

398
399
400
401
402
403
  describe '#diffs' do
    let(:merge_request) { build(:merge_request) }
    let(:options) { { paths: ['a/b', 'b/a', 'c/*'] } }

    context 'when there are MR diffs' do
      it 'delegates to the MR diffs' do
404
        merge_request.save
405

406
        expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
407
408
409
410
411
412

        merge_request.diffs(options)
      end
    end

    context 'when there are no MR diffs' do
413
      it 'delegates to the compare object, setting expanded: true' do
414
415
        merge_request.compare = double(:compare)

416
        expect(merge_request.compare).to receive(:diffs).with(options.merge(expanded: true))
417
418
419
420
421
422

        merge_request.diffs(options)
      end
    end
  end

Sean McGivern's avatar
Sean McGivern committed
423
424
425
426
427
428
  describe '#diff_size' do
    let(:merge_request) do
      build(:merge_request, source_branch: 'expand-collapse-files', target_branch: 'master')
    end

    context 'when there are MR diffs' do
429
      it 'returns the correct count' do
Sean McGivern's avatar
Sean McGivern committed
430
        merge_request.save
431
432

        expect(merge_request.diff_size).to eq('105')
Sean McGivern's avatar
Sean McGivern committed
433
434
      end

435
436
437
438
439
      it 'returns the correct overflow count' do
        allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
        merge_request.save

        expect(merge_request.diff_size).to eq('2+')
Sean McGivern's avatar
Sean McGivern committed
440
441
442
      end

      it 'does not perform highlighting' do
443
444
        merge_request.save

Sean McGivern's avatar
Sean McGivern committed
445
446
447
448
449
450
451
        expect(Gitlab::Diff::Highlight).not_to receive(:new)

        merge_request.diff_size
      end
    end

    context 'when there are no MR diffs' do
452
      def set_compare(merge_request)
Sean McGivern's avatar
Sean McGivern committed
453
454
455
456
457
458
459
460
461
462
        merge_request.compare = CompareService.new(
          merge_request.source_project,
          merge_request.source_branch
        ).execute(
          merge_request.target_project,
          merge_request.target_branch
        )
      end

      it 'returns the correct count' do
463
464
465
466
467
468
469
470
471
472
        set_compare(merge_request)

        expect(merge_request.diff_size).to eq('105')
      end

      it 'returns the correct overflow count' do
        allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
        set_compare(merge_request)

        expect(merge_request.diff_size).to eq('2+')
Sean McGivern's avatar
Sean McGivern committed
473
474
475
      end

      it 'does not perform highlighting' do
476
477
        set_compare(merge_request)

Sean McGivern's avatar
Sean McGivern committed
478
479
480
481
482
483
484
        expect(Gitlab::Diff::Highlight).not_to receive(:new)

        merge_request.diff_size
      end
    end
  end

485
  describe "#related_notes" do
486
    let!(:merge_request) { create(:merge_request) }
487
488

    before do
489
      allow(merge_request).to receive(:commits) { [merge_request.source_project.repository.commit] }
490
491
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.project)
492
      create(:note, noteable: merge_request, project: merge_request.project)
493
494
    end

495
    it "includes notes for commits" do
496
      expect(merge_request.commits).not_to be_empty
497
      expect(merge_request.related_notes.count).to eq(2)
498
    end
499

500
    it "includes notes for commits from target project as well" do
501
502
503
      create(:note_on_commit, commit_id: merge_request.commits.first.id,
                              project: merge_request.target_project)

504
      expect(merge_request.commits).not_to be_empty
505
      expect(merge_request.related_notes.count).to eq(3)
506
    end
507
  end
508

509
510
  describe '#for_fork?' do
    it 'returns true if the merge request is for a fork' do
511
512
      subject.source_project = build_stubbed(:project, namespace: create(:group))
      subject.target_project = build_stubbed(:project, namespace: create(:group))
513

514
      expect(subject.for_fork?).to be_truthy
515
    end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
516

517
    it 'returns false if is not for a fork' do
518
      expect(subject.for_fork?).to be_falsey
519
520
521
    end
  end

522
  describe '#closes_issues' do
523
524
    let(:issue0) { create :issue, project: subject.project }
    let(:issue1) { create :issue, project: subject.project }
525
526
527
528

    let(:commit0) { double('commit0', safe_message: "Fixes #{issue0.to_reference}") }
    let(:commit1) { double('commit1', safe_message: "Fixes #{issue0.to_reference}") }
    let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") }
529
530

    before do
531
      subject.project.add_developer(subject.author)
532
      allow(subject).to receive(:commits).and_return([commit0, commit1, commit2])
533
534
    end

535
    it 'accesses the set of issues that will be closed on acceptance' do
536
537
      allow(subject.project).to receive(:default_branch)
        .and_return(subject.target_branch)
538

539
      closed = subject.closes_issues
540

541
542
      expect(closed).to include(issue0, issue1)
    end
543

544
545
546
    it 'only lists issues as to be closed if it targets the default branch' do
      allow(subject.project).to receive(:default_branch).and_return('master')
      subject.target_branch = 'something-else'
547

548
      expect(subject.closes_issues).to be_empty
549
    end
550
  end
551

552
  describe '#issues_mentioned_but_not_closing' do
553
554
555
556
    let(:closing_issue) { create :issue, project: subject.project }
    let(:mentioned_issue) { create :issue, project: subject.project }

    let(:commit) { double('commit', safe_message: "Fixes #{closing_issue.to_reference}") }
557

558
    it 'detects issues mentioned in description but not closed' do
559
      subject.project.add_developer(subject.author)
560
      subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}"
561

562
      allow(subject).to receive(:commits).and_return([commit])
563
564
      allow(subject.project).to receive(:default_branch)
        .and_return(subject.target_branch)
565

566
      expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
567
    end
568
569
570

    context 'when the project has an external issue tracker' do
      before do
571
        subject.project.add_developer(subject.author)
572
573
574
575
576
577
578
579
580
581
582
583
584
        commit = double(:commit, safe_message: 'Fixes TEST-3')

        create(:jira_service, project: subject.project)

        allow(subject).to receive(:commits).and_return([commit])
        allow(subject).to receive(:description).and_return('Is related to TEST-2 and TEST-3')
        allow(subject.project).to receive(:default_branch).and_return(subject.target_branch)
      end

      it 'detects issues mentioned in description but not closed' do
        expect(subject.issues_mentioned_but_not_closing(subject.author).map(&:to_s)).to match_array(['TEST-2'])
      end
    end
585
586
  end

587
  describe "#work_in_progress?" do
588
589
590
    ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
      it "detects the '#{wip_prefix}' prefix" do
        subject.title = "#{wip_prefix}#{subject.title}"
591
        expect(subject.work_in_progress?).to eq true
592
      end
593
594
    end

595
596
    it "doesn't detect WIP for words starting with WIP" do
      subject.title = "Wipwap #{subject.title}"
597
      expect(subject.work_in_progress?).to eq false
598
599
    end

600
601
    it "doesn't detect WIP for words containing with WIP" do
      subject.title = "WupWipwap #{subject.title}"
602
      expect(subject.work_in_progress?).to eq false
603
604
    end

605
    it "doesn't detect WIP by default" do
606
      expect(subject.work_in_progress?).to eq false
607
608
609
    end
  end

Thomas Balthazar's avatar
Thomas Balthazar committed
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
  describe "#wipless_title" do
    ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
      it "removes the '#{wip_prefix}' prefix" do
        wipless_title = subject.title
        subject.title = "#{wip_prefix}#{subject.title}"

        expect(subject.wipless_title).to eq wipless_title
      end

      it "is satisfies the #work_in_progress? method" do
        subject.title = "#{wip_prefix}#{subject.title}"
        subject.title = subject.wipless_title

        expect(subject.work_in_progress?).to eq false
      end
    end
  end

  describe "#wip_title" do
    it "adds the WIP: prefix to the title" do
      wip_title = "WIP: #{subject.title}"

      expect(subject.wip_title).to eq wip_title
633
    end
Thomas Balthazar's avatar
Thomas Balthazar committed
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649

    it "does not add the WIP: prefix multiple times" do
      wip_title = "WIP: #{subject.title}"
      subject.title = subject.wip_title
      subject.title = subject.wip_title

      expect(subject.wip_title).to eq wip_title
    end

    it "is satisfies the #work_in_progress? method" do
      subject.title = subject.wip_title

      expect(subject.work_in_progress?).to eq true
    end
  end

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
650
  describe '#can_remove_source_branch?' do
651
652
    set(:user) { create(:user) }
    set(:merge_request) { create(:merge_request, :simple) }
653

654
    subject { merge_request }
655

656
657
    before do
      subject.source_project.add_master(user)
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
658
    end
659

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
660
    it "can't be removed when its a protected branch" do
661
      allow(ProtectedBranch).to receive(:protected?).and_return(true)
662

663
664
665
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end

666
    it "can't remove a root ref" do
667
      subject.update(source_branch: 'master', target_branch: 'feature')
668
669
670
671

      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
672
    it "is unable to remove the source branch for a project the user cannot push to" do
673
674
      user2 = create(:user)

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
675
676
677
      expect(subject.can_remove_source_branch?(user2)).to be_falsey
    end

678
    it "can be removed if the last commit is the head of the source branch" do
679
      allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit)
680

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
681
      expect(subject.can_remove_source_branch?(user)).to be_truthy
682
    end
683
684

    it "cannot be removed if the last commit is not also the head of the source branch" do
685
      subject.clear_memoized_shas
686
687
      subject.source_branch = "lfs"

688
689
      expect(subject.can_remove_source_branch?(user)).to be_falsey
    end
690
691
  end

692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
  describe '#merge_commit_message' do
    it 'includes merge information as the title' do
      request = build(:merge_request, source_branch: 'source', target_branch: 'target')

      expect(request.merge_commit_message)
        .to match("Merge branch 'source' into 'target'\n\n")
    end

    it 'includes its title in the body' do
      request = build(:merge_request, title: 'Remove all technical debt')

      expect(request.merge_commit_message)
        .to match("Remove all technical debt\n\n")
    end

707
    it 'includes its closed issues in the body' do
708
      issue = create(:issue, project: subject.project)
709

710
      subject.project.add_developer(subject.author)
711
      subject.description = "This issue Closes #{issue.to_reference}"
712

713
714
      allow(subject.project).to receive(:default_branch)
        .and_return(subject.target_branch)
715
716

      expect(subject.merge_commit_message)
717
        .to match("Closes #{issue.to_reference}")
718
719
720
721
722
723
    end

    it 'includes its reference in the body' do
      request = build_stubbed(:merge_request)

      expect(request.merge_commit_message)
724
        .to match("See merge request #{request.to_reference(full: true)}")
725
726
727
728
729
730
731
    end

    it 'excludes multiple linebreak runs when description is blank' do
      request = build(:merge_request, title: 'Title', description: nil)

      expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
    end
732
733
734
735
736
737
738
739
740
741
742
743
744
745

    it 'includes its description in the body' do
      request = build(:merge_request, description: 'By removing all code')

      expect(request.merge_commit_message(include_description: true))
        .to match("By removing all code\n\n")
    end

    it 'does not includes its description in the body' do
      request = build(:merge_request, description: 'By removing all code')

      expect(request.merge_commit_message)
        .not_to match("By removing all code\n\n")
    end
746
747
  end

748
  describe "#reset_merge_when_pipeline_succeeds" do
749
    let(:merge_if_green) do
750
      create :merge_request, merge_when_pipeline_succeeds: true, merge_user: create(:user),
751
752
                             merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
    end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
753

754
    it "sets the item to false" do
755
      merge_if_green.reset_merge_when_pipeline_succeeds
756
757
      merge_if_green.reload

758
      expect(merge_if_green.merge_when_pipeline_succeeds).to be_falsey
759
760
      expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
      expect(merge_if_green.merge_params["commit_message"]).to be_nil
761
762
763
    end
  end

764
  describe '#hook_attrs' do
765
766
767
768
769
770
771
772
    it 'delegates to Gitlab::HookData::MergeRequestBuilder#build' do
      builder = double

      expect(Gitlab::HookData::MergeRequestBuilder)
        .to receive(:new).with(subject).and_return(builder)
      expect(builder).to receive(:build)

      subject.hook_attrs
773
    end
774
775
776
  end

  describe '#diverged_commits_count' do
777
    let(:project)      { create(:project, :repository) }
778
    let(:forked_project) { fork_project(project, nil, repository: true) }
779

780
    context 'when the target branch does not exist anymore' do
781
782
783
784
      subject { create(:merge_request, source_project: project, target_project: project) }

      before do
        project.repository.raw_repository.delete_branch(subject.target_branch)
785
        subject.clear_memoized_shas
786
      end
787
788

      it 'does not crash' do
789
        expect { subject.diverged_commits_count }.not_to raise_error
790
791
792
793
794
795
796
      end

      it 'returns 0' do
        expect(subject.diverged_commits_count).to eq(0)
      end
    end

797
798
799
800
    context 'diverged on same repository' do
      subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) }

      it 'counts commits that are on target branch but not on source branch' do
801
        expect(subject.diverged_commits_count).to eq(29)
802
803
804
805
      end
    end

    context 'diverged on fork' do
806
      subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) }
807
808

      it 'counts commits that are on target branch but not on source branch' do
809
        expect(subject.diverged_commits_count).to eq(29)
810
811
812
813
      end
    end

    context 'rebased on fork' do
814
      subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) }
815
816
817
818
819
820
821

      it 'counts commits that are on target branch but not on source branch' do
        expect(subject.diverged_commits_count).to eq(0)
      end
    end

    describe 'caching' do
822
      before do
823
824
825
826
        allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
      end

      it 'caches the output' do
827
828
829
        expect(subject).to receive(:compute_diverged_commits_count)
          .once
          .and_return(2)
830
831
832
833
834
835

        subject.diverged_commits_count
        subject.diverged_commits_count
      end

      it 'invalidates the cache when the source sha changes' do
836
837
838
        expect(subject).to receive(:compute_diverged_commits_count)
          .twice
          .and_return(2)
839
840

        subject.diverged_commits_count
841
        allow(subject).to receive(:source_branch_sha).and_return('123abc')
842
843
844
845
        subject.diverged_commits_count
      end

      it 'invalidates the cache when the target sha changes' do
846
847
848
        expect(subject).to receive(:compute_diverged_commits_count)
          .twice
          .and_return(2)
849
850

        subject.diverged_commits_count
851
        allow(subject).to receive(:target_branch_sha).and_return('123abc')
852
853
854
        subject.diverged_commits_count
      end
    end
855
856
  end

857
  it_behaves_like 'an editable mentionable' do
858
    subject { create(:merge_request, :simple) }
859

860
    let(:backref_text) { "merge request #{subject.to_reference}" }
861
    let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
862
  end
Vinnie Okada's avatar
Vinnie Okada committed
863
864

  it_behaves_like 'a Taskable' do
865
    subject { create :merge_request, :simple }
Vinnie Okada's avatar
Vinnie Okada committed
866
  end
867

868
  describe '#commit_shas' do
869
    before do
870
      allow(subject.merge_request_diff).to receive(:commit_shas)
871
        .and_return(['sha1'])
872
873
    end

874
    it 'delegates to merge request diff' do
875
      expect(subject.commit_shas).to eq ['sha1']
876
877
878
    end
  end

879
  context 'head pipeline' do
880
881
882
    before do
      allow(subject).to receive(:diff_head_sha).and_return('lastsha')
    end
883

884
885
886
    describe '#head_pipeline' do
      it 'returns nil for MR without head_pipeline_id' do
        subject.update_attribute(:head_pipeline_id, nil)
887

888
889
        expect(subject.head_pipeline).to be_nil
      end
890
891
892
893
894
895
896
897

      context 'when the source project does not exist' do
        it 'returns nil' do
          allow(subject).to receive(:source_project).and_return(nil)

          expect(subject.head_pipeline).to be_nil
        end
      end
898
899
    end

900
    describe '#actual_head_pipeline' do
901
902
903
      it 'returns nil for MR with old pipeline' do
        pipeline = create(:ci_empty_pipeline, sha: 'notlatestsha')
        subject.update_attribute(:head_pipeline_id, pipeline.id)
904

905
        expect(subject.actual_head_pipeline).to be_nil
906
      end
907

908
909
910
      it 'returns the pipeline for MR with recent pipeline' do
        pipeline = create(:ci_empty_pipeline, sha: 'lastsha')
        subject.update_attribute(:head_pipeline_id, pipeline.id)
911

912
913
        expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
        expect(subject.actual_head_pipeline).to eq(pipeline)
914
      end
915
916
917
918
919
920

      it 'returns nil when source project does not exist' do
        allow(subject).to receive(:source_project).and_return(nil)

        expect(subject.actual_head_pipeline).to be_nil
      end
921
922
    end
  end
Yorick Peterse's avatar
Yorick Peterse committed
923

924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
  describe '#has_ci?' do
    let(:merge_request) { build_stubbed(:merge_request) }

    context 'has ci' do
      it 'returns true if MR has head_pipeline_id and commits' do
        allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
        allow(merge_request).to receive(:head_pipeline_id) { double }
        allow(merge_request).to receive(:has_no_commits?) { false }

        expect(merge_request.has_ci?).to be(true)
      end

      it 'returns true if MR has any pipeline and commits' do
        allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
        allow(merge_request).to receive(:head_pipeline_id) { nil }
        allow(merge_request).to receive(:has_no_commits?) { false }
        allow(merge_request).to receive(:all_pipelines) { [double] }

        expect(merge_request.has_ci?).to be(true)
      end

      it 'returns true if MR has CI service and commits' do
        allow(merge_request).to receive_message_chain(:source_project, :ci_service) { double }
        allow(merge_request).to receive(:head_pipeline_id) { nil }
        allow(merge_request).to receive(:has_no_commits?) { false }
        allow(merge_request).to receive(:all_pipelines) { [] }

        expect(merge_request.has_ci?).to be(true)
      end
    end

    context 'has no ci' do
      it 'returns false if MR has no CI service nor pipeline, and no commits' do
        allow(merge_request).to receive_message_chain(:source_project, :ci_service) { nil }
        allow(merge_request).to receive(:head_pipeline_id) { nil }
        allow(merge_request).to receive(:all_pipelines) { [] }
        allow(merge_request).to receive(:has_no_commits?) { true }

        expect(merge_request.has_ci?).to be(false)
      end
    end
  end

967
  describe '#all_pipelines' do
968
    shared_examples 'returning pipelines with proper ordering' do
969
      let!(:all_pipelines) do
970
        subject.all_commit_shas.map do |sha|
971
972
973
974
          create(:ci_empty_pipeline,
                 project: subject.source_project,
                 sha: sha,
                 ref: subject.source_branch)
975
976
977
978
979
        end
      end

      it 'returns all pipelines' do
        expect(subject.all_pipelines).not_to be_empty
980
        expect(subject.all_pipelines).to eq(all_pipelines.reverse)
981
      end
982
983
    end

984
985
986
987
988
989
    context 'with single merge_request_diffs' do
      it_behaves_like 'returning pipelines with proper ordering'
    end

    context 'with multiple irrelevant merge_request_diffs' do
      before do
990
        subject.update(target_branch: 'v1.0.0')
991
992
993
      end

      it_behaves_like 'returning pipelines with proper ordering'
994
    end
995
996

    context 'with unsaved merge request' do
997
      subject { build(:merge_request) }
998
999
1000

      let!(:pipeline) do
        create(:ci_empty_pipeline,
1001
               project: subject.project,
1002
1003
               sha: subject.diff_head_sha,
               ref: subject.source_branch)
1004
      end
1005
1006
1007
1008
1009

      it 'returns pipelines from diff_head_sha' do
        expect(subject.all_pipelines).to contain_exactly(pipeline)
      end
    end
1010
1011
  end

1012
  describe '#all_commit_shas' do
1013
    context 'when merge request is persisted' do
1014
      let(:all_commit_shas) do
1015
1016
        subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
      end
1017

1018
      shared_examples 'returning all SHA' do
1019
        it 'returns all SHAs from all merge_request_diffs' do
1020
          expect(subject.merge_request_diffs.size).to eq(2)
1021
          expect(subject.all_commit_shas).to match_array(all_commit_shas)
1022
        end
1023
1024
      end

1025
1026
      context 'with a completely different branch' do
        before do
1027
          subject.update(target_branch: 'csv')
1028
1029
1030
        end

        it_behaves_like 'returning all SHA'
1031
1032
      end

1033
1034
      context 'with a branch having no difference' do
        before do
1035
          subject.update(target_branch: 'branch-merged')
1036
1037
1038
1039
1040
          subject.reload # make sure commits were not cached
        end

        it_behaves_like 'returning all SHA'
      end
1041
1042
    end

1043
1044
1045
1046
1047
1048
1049
1050
1051
    context 'when merge request is not persisted' do
      context 'when compare commits are set in the service' do
        let(:commit) { spy('commit') }

        subject do
          build(:merge_request, compare_commits: [commit, commit])
        end

        it 'returns commits from compare commits temporary data' do
1052
          expect(subject.all_commit_shas).to eq [commit, commit]
1053
        end
1054
1055
      end

1056
1057
1058
1059
      context 'when compare commits are not set in the service' do
        subject { build(:merge_request) }

        it 'returns array with diff head sha element only' do
1060
          expect(subject.all_commit_shas).to eq [subject.diff_head_sha]
1061
1062
        end
      end
1063
1064
1065
    end
  end

1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
  describe '#can_be_reverted?' do
    context 'when there is no merge_commit for the MR' do
      before do
        subject.metrics.update!(merged_at: Time.now.utc)
      end

      it 'returns false' do
        expect(subject.can_be_reverted?(nil)).to be_falsey
      end
    end

    context 'when the MR has been merged' do
      before do
        MergeRequests::MergeService
          .new(subject.target_project, subject.author)
          .execute(subject)
      end

      context 'when there is no revert commit' do
        it 'returns true' do
          expect(subject.can_be_reverted?(nil)).to be_truthy
        end
      end

1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
      context 'when there is no merged_at for the MR' do
        before do
          subject.metrics.update!(merged_at: nil)
        end

        it 'returns true' do
          expect(subject.can_be_reverted?(nil)).to be_truthy
        end
      end

1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1