commit_spec.rb 16.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 186
    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(:tree) }
    it { is_expected.to respond_to(:id) }
    it { is_expected.to respond_to(:to_patch) }
187
  end
188 189 190

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

    before do
      project.team << [commiter, :developer]
      other_project.team << [commiter, :developer]
    end
199 200

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

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

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

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

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

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

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

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

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

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

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

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

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

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

263 264
        repository.commit(merge_commit_id)
      end
265

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

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

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

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

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

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

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

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

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

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

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

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

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

320
      it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy }
321 322 323 324 325 326 327 328 329
    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

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

334
  describe '#last_pipeline' do
335 336 337 338 339 340 341 342 343 344 345 346 347
    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

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

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

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

370 371 372 373 374 375 376 377
    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
378

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

387 388 389 390
      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
391

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

398 399 400 401 402 403 404 405
  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
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 440
  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
441 442 443 444 445

  describe '#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)
446
      expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw)
447 448 449 450 451 452 453
      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
454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470

  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
471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498

  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
499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516

  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
517
end