commit_spec.rb 17.5 KB
Newer Older
1 2
require 'spec_helper'

3
describe Commit do
4
  let(:project) { create(:project, :public, :repository) }
5 6 7 8 9 10 11 12 13 14 15
  let(:commit)  { project.commit }

  describe 'modules' do
    subject { described_class }

    it { is_expected.to include_module(Mentionable) }
    it { is_expected.to include_module(Participable) }
    it { is_expected.to include_module(Referable) }
    it { is_expected.to include_module(StaticModel) }
  end

16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
  describe '.lazy' do
    set(:project) { create(:project, :repository) }

    context 'when the commits are found' do
      let(:oids) do
        %w(
          498214de67004b1da3d820901307bed2a68a8ef6
          c642fe9b8b9f28f9225d7ea953fe14e74748d53b
          6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
          048721d90c449b244b7b4c53a9186b04330174ec
          281d3a76f31c812dbf48abce82ccf6860adedd81
        )
      end

      subject { oids.map { |oid| described_class.lazy(project, oid) } }

      it 'batches requests for commits' do
        expect(project.repository).to receive(:commits_by).once.and_call_original

        subject.first.title
        subject.last.title
      end

      it 'maintains ordering' do
        subject.each_with_index do |commit, i|
          expect(commit.id).to eq(oids[i])
        end
      end
    end

    context 'when not found' do
      it 'returns nil as commit' do
        commit = described_class.lazy(project, 'deadbeef').__sync

        expect(commit).to be_nil
      end
    end
  end

55 56 57 58 59 60
  describe '#author' do
    it 'looks up the author in a case-insensitive way' do
      user = create(:user, email: commit.author_email.upcase)
      expect(commit.author).to eq(user)
    end

61
    it 'caches the author', :request_store do
62
      user = create(:user, email: commit.author_email)
63
      expect(User).to receive(:find_by_any_email).and_call_original
64 65

      expect(commit.author).to eq(user)
66
      key = "Commit:author:#{commit.author_email.downcase}"
67 68 69 70 71 72
      expect(RequestStore.store[key]).to eq(user)

      expect(commit.author).to eq(user)
    end
  end

73
  describe '#to_reference' do
74
    let(:project) { create(:project, :repository, path: 'sample-project') }
75

76
    it 'returns a String reference to the object' do
77
      expect(commit.to_reference).to eq commit.id
78 79 80
    end

    it 'supports a cross-project reference' do
81
      another_project = build(:project, :repository, name: 'another-project', namespace: project.namespace)
82
      expect(commit.to_reference(another_project)).to eq "sample-project@#{commit.id}"
83 84 85 86
    end
  end

  describe '#reference_link_text' do
87
    let(:project) { create(:project, :repository, path: 'sample-project') }
88

89 90 91 92 93
    it 'returns a String reference to the object' do
      expect(commit.reference_link_text).to eq commit.short_id
    end

    it 'supports a cross-project reference' do
94
      another_project = build(:project, :repository, name: 'another-project', namespace: project.namespace)
95
      expect(commit.reference_link_text(another_project)).to eq "sample-project@#{commit.short_id}"
96 97
    end
  end
98

99 100
  describe '#title' do
    it "returns no_commit_message when safe_message is blank" do
101 102
      allow(commit).to receive(:safe_message).and_return('')
      expect(commit.title).to eq("--no commit message")
103
    end
104

105
    it 'truncates a message without a newline at natural break to 80 characters' do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
106
      message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
107

108
      allow(commit).to receive(:safe_message).and_return(message)
109
      expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis…')
110
    end
111

112 113
    it "truncates a message with a newline before 80 characters at the newline" do
      message = commit.safe_message.split(" ").first
114

115 116
      allow(commit).to receive(:safe_message).and_return(message + "\n" + message)
      expect(commit.title).to eq(message)
117
    end
118

Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
119
    it "does not truncates a message with a newline after 80 but less 100 characters" do
120
      message = <<eos
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
121 122 123
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit.
Vivamus egestas lacinia lacus, sed rutrum mauris.
eos
124

125 126
      allow(commit).to receive(:safe_message).and_return(message)
      expect(commit.title).to eq(message.split("\n").first)
127 128
    end
  end
129

130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150
  describe '#full_title' do
    it "returns no_commit_message when safe_message is blank" do
      allow(commit).to receive(:safe_message).and_return('')
      expect(commit.full_title).to eq("--no commit message")
    end

    it "returns entire message if there is no newline" do
      message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'

      allow(commit).to receive(:safe_message).and_return(message)
      expect(commit.full_title).to eq(message)
    end

    it "returns first line of message if there is a newLine" do
      message = commit.safe_message.split(" ").first

      allow(commit).to receive(:safe_message).and_return(message + "\n" + message)
      expect(commit.full_title).to eq(message)
    end
  end

151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
  describe 'description' do
    it 'returns description of commit message if title less than 100 characters' do
      message = <<eos
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit.
Vivamus egestas lacinia lacus, sed rutrum mauris.
eos

      allow(commit).to receive(:safe_message).and_return(message)
      expect(commit.description).to eq('Vivamus egestas lacinia lacus, sed rutrum mauris.')
    end

    it 'returns full commit message if commit title more than 100 characters' do
      message = <<eos
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.
Vivamus egestas lacinia lacus, sed rutrum mauris.
eos

      allow(commit).to receive(:safe_message).and_return(message)
      expect(commit.description).to eq(message)
    end
  end

173 174 175
  describe "delegation" do
    subject { commit }

176 177 178 179 180 181 182 183 184 185
    it { is_expected.to respond_to(:message) }
    it { is_expected.to respond_to(:authored_date) }
    it { is_expected.to respond_to(:committed_date) }
    it { is_expected.to respond_to(:committer_email) }
    it { is_expected.to respond_to(:author_email) }
    it { is_expected.to respond_to(:parents) }
    it { is_expected.to respond_to(:date) }
    it { is_expected.to respond_to(:diffs) }
    it { is_expected.to respond_to(:id) }
    it { is_expected.to respond_to(:to_patch) }
186
  end
187 188 189

  describe '#closes_issues' do
    let(:issue) { create :issue, project: project }
190
    let(:other_project) { create(:project, :public) }
191
    let(:other_issue) { create :issue, project: other_project }
192 193 194
    let(:commiter) { create :user }

    before do
195 196
      project.add_developer(commiter)
      other_project.add_developer(commiter)
197
    end
198 199

    it 'detects issues that this commit is marked as closing' do
200
      ext_ref = "#{other_project.full_path}##{other_issue.iid}"
201 202 203 204 205 206

      allow(commit).to receive_messages(
        safe_message: "Fixes ##{issue.iid} and #{ext_ref}",
        committer_email: commiter.email
      )

Douwe Maan's avatar
Douwe Maan committed
207 208
      expect(commit.closes_issues).to include(issue)
      expect(commit.closes_issues).to include(other_issue)
209
    end
210 211 212
  end

  it_behaves_like 'a mentionable' do
213
    subject { create(:project, :repository).commit }
214

Douwe Maan's avatar
Douwe Maan committed
215
    let(:author) { create(:user, email: subject.author_email) }
216
    let(:backref_text) { "commit #{subject.id}" }
217 218 219
    let(:set_mentionable_text) do
      ->(txt) { allow(subject).to receive(:safe_message).and_return(txt) }
    end
220 221 222 223

    # Include the subject in the repository stub.
    let(:extra_commits) { [subject] }
  end
224 225

  describe '#hook_attrs' do
Valery Sizov's avatar
Valery Sizov committed
226
    let(:data) { commit.hook_attrs(with_changed_files: true) }
227 228

    it { expect(data).to be_a(Hash) }
229
    it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') }
230
    it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') }
231
    it { expect(data[:added]).to contain_exactly("bar/branch-test.txt") }
232
    it { expect(data[:modified]).to eq([]) }
233 234
    it { expect(data[:removed]).to eq([]) }
  end
235

236
  describe '#cherry_pick_message' do
237
    let(:user) { create(:user) }
238 239

    context 'of a regular commit' do
240 241 242
      let(:commit) { project.commit('video') }

      it { expect(commit.cherry_pick_message(user)).to include("\n\n(cherry picked from commit 88790590ed1337ab189bccaa355f068481c90bec)") }
243 244 245
    end

    context 'of a merge commit' do
246 247
      let(:repository) { project.repository }

248 249 250 251 252 253 254
      let(:merge_request) do
        create(:merge_request,
               source_branch: 'video',
               target_branch: 'master',
               source_project: project,
               author: user)
      end
255

256
      let(:merge_commit) do
257 258 259
        merge_commit_id = repository.merge(user,
                                           merge_request.diff_head_sha,
                                           merge_request,
260
                                           'Test message')
261

262 263
        repository.commit(merge_commit_id)
      end
264

265 266 267 268 269
      context 'that is found' do
        before do
          # Artificially mark as completed.
          merge_request.update(merge_commit_sha: merge_commit.id)
        end
270

271 272
        it do
          expected_appended_text = <<~STR.rstrip
273

274
            (cherry picked from commit #{merge_commit.sha})
275

276 277 278
            467dc98f Add new 'videos' directory
            88790590 Upload new video file
          STR
279

280 281 282
          expect(merge_commit.cherry_pick_message(user)).to include(expected_appended_text)
        end
      end
283

284 285
      context "that is existing but not found" do
        it 'does not include details of the merged commits' do
286 287
          expect(merge_commit.cherry_pick_message(user)).to end_with("(cherry picked from commit #{merge_commit.sha})")
        end
288 289 290 291
      end
    end
  end

292 293
  describe '#reverts_commit?' do
    let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") }
294
    let(:user) { commit.author }
295

296
    it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
297 298

    context 'commit has no description' do
299 300 301
      before do
        allow(commit).to receive(:description?).and_return(false)
      end
302

303
      it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
304 305 306
    end

    context "another_commit's description does not revert commit" do
307 308 309
      before do
        allow(commit).to receive(:description).and_return("Foo Bar")
      end
310

311
      it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
312 313 314
    end

    context "another_commit's description reverts commit" do
315 316 317
      before do
        allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar")
      end
318

319
      it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy }
320 321 322 323 324 325 326 327 328
    end

    context "another_commit's description reverts merged merge request" do
      before do
        revert_description = "This reverts merge request !foo123"
        allow(another_commit).to receive(:revert_description).and_return(revert_description)
        allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar")
      end

329
      it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy }
330 331
    end
  end
332

333
  describe '#last_pipeline' do
334 335 336 337 338 339 340 341 342 343 344 345 346
    let!(:first_pipeline) do
      create(:ci_empty_pipeline,
        project: project,
        sha: commit.sha,
        status: 'success')
    end
    let!(:second_pipeline) do
      create(:ci_empty_pipeline,
        project: project,
        sha: commit.sha,
        status: 'success')
    end

347 348
    it 'returns last pipeline' do
      expect(commit.last_pipeline).to eq second_pipeline
349 350 351
    end
  end

352
  describe '#status' do
353
    context 'without ref argument' do
354
      before do
355
        %w[success failed created pending].each do |status|
356 357 358
          create(:ci_empty_pipeline,
                 project: project,
                 sha: commit.sha,
359
                 status: status)
360 361
        end
      end
362

363
      it 'gives compound status from latest pipelines' do
364
        expect(commit.status).to eq(Ci::Pipeline.latest_status)
365
        expect(commit.status).to eq('pending')
366
      end
367 368
    end

369 370 371 372 373 374 375 376
    context 'when a particular ref is specified' do
      let!(:pipeline_from_master) do
        create(:ci_empty_pipeline,
               project: project,
               sha: commit.sha,
               ref: 'master',
               status: 'failed')
      end
377

378 379 380 381 382 383 384
      let!(:pipeline_from_fix) do
        create(:ci_empty_pipeline,
               project: project,
               sha: commit.sha,
               ref: 'fix',
               status: 'success')
      end
385

386 387 388 389
      it 'gives pipelines from a particular branch' do
        expect(commit.status('master')).to eq(pipeline_from_master.status)
        expect(commit.status('fix')).to eq(pipeline_from_fix.status)
      end
390

391
      it 'gives compound status from latest pipelines if ref is nil' do
392
        expect(commit.status(nil)).to eq(pipeline_from_fix.status)
393
      end
394
    end
395
  end
Yorick Peterse's avatar
Yorick Peterse committed
396

397 398 399 400 401 402 403 404
  describe '#set_status_for_ref' do
    it 'sets the status for a given reference' do
      commit.set_status_for_ref('master', 'failed')

      expect(commit.status('master')).to eq('failed')
    end
  end

Yorick Peterse's avatar
Yorick Peterse committed
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
  describe '#participants' do
    let(:user1) { build(:user) }
    let(:user2) { build(:user) }

    let!(:note1) do
      create(:note_on_commit,
             commit_id: commit.id,
             project: project,
             note: 'foo')
    end

    let!(:note2) do
      create(:note_on_commit,
             commit_id: commit.id,
             project: project,
             note: 'bar')
    end

    before do
      allow(commit).to receive(:author).and_return(user1)
      allow(commit).to receive(:committer).and_return(user2)
    end

    it 'includes the commit author' do
      expect(commit.participants).to include(commit.author)
    end

    it 'includes the committer' do
      expect(commit.participants).to include(commit.committer)
    end

    it 'includes the authors of the commit notes' do
      expect(commit.participants).to include(note1.author, note2.author)
    end
  end
440 441

  describe '#uri_type' do
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
    shared_examples 'URI type' do
      it 'returns the URI type at the given path' do
        expect(commit.uri_type('files/html')).to be(:tree)
        expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
        expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
        expect(commit.uri_type('files/js/application.js')).to be(:blob)
      end

      it "returns nil if the path doesn't exists" do
        expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
      end
    end

    context 'when Gitaly commit_tree_entry feature is enabled' do
      it_behaves_like 'URI type'
457 458
    end

459 460
    context 'when Gitaly commit_tree_entry feature is disabled', :disable_gitaly do
      it_behaves_like 'URI type'
461 462
    end
  end
463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479

  describe '.from_hash' do
    let(:new_commit) { described_class.from_hash(commit.to_hash, project) }

    it 'returns a Commit' do
      expect(new_commit).to be_an_instance_of(described_class)
    end

    it 'wraps a Gitlab::Git::Commit' do
      expect(new_commit.raw).to be_an_instance_of(Gitlab::Git::Commit)
    end

    it 'stores the correct commit fields' do
      expect(new_commit.id).to eq(commit.id)
      expect(new_commit.message).to eq(commit.message)
    end
  end
480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507

  describe '#work_in_progress?' do
    ['squash! ', 'fixup! ', 'wip: ', 'WIP: ', '[WIP] '].each do |wip_prefix|
      it "detects the '#{wip_prefix}' prefix" do
        commit.message = "#{wip_prefix}#{commit.message}"

        expect(commit).to be_work_in_progress
      end
    end

    it "detects WIP for a commit just saying 'wip'" do
      commit.message = "wip"

      expect(commit).to be_work_in_progress
    end

    it "doesn't detect WIP for a commit that begins with 'FIXUP! '" do
      commit.message = "FIXUP! #{commit.message}"

      expect(commit).not_to be_work_in_progress
    end

    it "doesn't detect WIP for words starting with WIP" do
      commit.message = "Wipout #{commit.message}"

      expect(commit).not_to be_work_in_progress
    end
  end
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525

  describe '.valid_hash?' do
    it 'checks hash contents' do
      expect(described_class.valid_hash?('abcdef01239ABCDEF')).to be true
      expect(described_class.valid_hash?("abcdef01239ABCD\nEF")).to be false
      expect(described_class.valid_hash?(' abcdef01239ABCDEF ')).to be false
      expect(described_class.valid_hash?('Gabcdef01239ABCDEF')).to be false
      expect(described_class.valid_hash?('gabcdef01239ABCDEF')).to be false
      expect(described_class.valid_hash?('-abcdef01239ABCDEF')).to be false
    end

    it 'checks hash length' do
      expect(described_class.valid_hash?('a' * 6)).to be false
      expect(described_class.valid_hash?('a' * 7)).to be true
      expect(described_class.valid_hash?('a' * 40)).to be true
      expect(described_class.valid_hash?('a' * 41)).to be false
    end
  end
526 527 528 529 530 531 532 533 534

  describe '#merge_requests' do
    let!(:project) { create(:project, :repository) }
    let!(:merge_request1) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
    let!(:merge_request2) { create(:merge_request, source_project: project, source_branch: 'merged-target', target_branch: 'feature') }
    let(:commit1) { merge_request1.merge_request_diff.commits.last }
    let(:commit2) { merge_request1.merge_request_diff.commits.first }

    it 'returns merge_requests that introduced that commit' do
535 536
      expect(commit1.merge_requests).to contain_exactly(merge_request1, merge_request2)
      expect(commit2.merge_requests).to contain_exactly(merge_request1)
537 538
    end
  end
539
end